Merge branch 'canary' into feat/quick-service-switcher

This commit is contained in:
Mauricio Siu
2026-03-19 00:43:03 -06:00
169 changed files with 91767 additions and 28607 deletions

View File

@@ -27,8 +27,11 @@ import { portRouter } from "./routers/port";
import { postgresRouter } from "./routers/postgres";
import { previewDeploymentRouter } from "./routers/preview-deployment";
import { projectRouter } from "./routers/project";
import { auditLogRouter } from "./routers/proprietary/audit-log";
import { customRoleRouter } from "./routers/proprietary/custom-role";
import { licenseKeyRouter } from "./routers/proprietary/license-key";
import { ssoRouter } from "./routers/proprietary/sso";
import { whitelabelingRouter } from "./routers/proprietary/whitelabeling";
import { redirectsRouter } from "./routers/redirects";
import { redisRouter } from "./routers/redis";
import { registryRouter } from "./routers/registry";
@@ -87,6 +90,9 @@ export const appRouter = createTRPCRouter({
organization: organizationRouter,
licenseKey: licenseKeyRouter,
sso: ssoRouter,
whitelabeling: whitelabelingRouter,
customRole: customRoleRouter,
auditLog: auditLogRouter,
schedule: scheduleRouter,
rollback: rollbackRouter,
volumeBackups: volumeBackupsRouter,

View File

@@ -21,7 +21,7 @@ import { findProjectById } from "@dokploy/server/services/project";
import {
addNewService,
checkServiceAccess,
} from "@dokploy/server/services/user";
} from "@dokploy/server/services/permission";
import {
getProviderHeaders,
getProviderName,
@@ -38,17 +38,10 @@ import {
import { generatePassword } from "@/templates/utils";
export const aiRouter = createTRPCRouter({
one: protectedProcedure
one: adminProcedure
.input(z.object({ aiId: z.string() }))
.query(async ({ ctx, input }) => {
const aiSetting = await getAiSettingById(input.aiId);
if (aiSetting.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this AI configuration",
});
}
return aiSetting;
.query(async ({ input }) => {
return await getAiSettingById(input.aiId);
}),
getModels: protectedProcedure
@@ -159,11 +152,9 @@ export const aiRouter = createTRPCRouter({
return await saveAiSettings(ctx.session.activeOrganizationId, input);
}),
update: protectedProcedure
.input(apiUpdateAi)
.mutation(async ({ ctx, input }) => {
return await saveAiSettings(ctx.session.activeOrganizationId, input);
}),
update: adminProcedure.input(apiUpdateAi).mutation(async ({ ctx, input }) => {
return await saveAiSettings(ctx.session.activeOrganizationId, input);
}),
getAll: adminProcedure.query(async ({ ctx }) => {
return await getAiSettingsByOrganizationId(
@@ -171,29 +162,15 @@ export const aiRouter = createTRPCRouter({
);
}),
get: protectedProcedure
get: adminProcedure
.input(z.object({ aiId: z.string() }))
.query(async ({ ctx, input }) => {
const aiSetting = await getAiSettingById(input.aiId);
if (aiSetting.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this AI configuration",
});
}
return aiSetting;
.query(async ({ input }) => {
return await getAiSettingById(input.aiId);
}),
delete: protectedProcedure
delete: adminProcedure
.input(z.object({ aiId: z.string() }))
.mutation(async ({ ctx, input }) => {
const aiSetting = await getAiSettingById(input.aiId);
if (aiSetting.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this AI configuration",
});
}
.mutation(async ({ input }) => {
return await deleteAiSettings(input.aiId);
}),
@@ -223,13 +200,7 @@ export const aiRouter = createTRPCRouter({
.mutation(async ({ ctx, input }) => {
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.session.activeOrganizationId,
environment.projectId,
"create",
);
}
await checkServiceAccess(ctx, environment.projectId, "create");
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
@@ -275,13 +246,7 @@ export const aiRouter = createTRPCRouter({
}
}
if (ctx.user.role === "member") {
await addNewService(
ctx.session.activeOrganizationId,
ctx.user.ownerId,
compose.composeId,
);
}
await addNewService(ctx, compose.composeId);
return null;
}),

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,13 @@ import {
} from "@dokploy/server/utils/restore";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
createTRPCRouter,
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateBackup,
apiFindOneBackup,
@@ -69,10 +75,21 @@ interface RcloneFile {
export const backupRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateBackup)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
const newBackup = await createBackup(input);
const serviceId =
input.postgresId ||
input.mysqlId ||
input.mariadbId ||
input.mongoId ||
input.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
backup: ["create"],
});
}
const newBackup = await createBackup(input);
const backup = await findBackupById(newBackup.backupId);
if (IS_CLOUD && backup.enabled) {
@@ -110,6 +127,11 @@ export const backupRouter = createTRPCRouter({
scheduleBackup(backup);
}
}
await audit(ctx, {
action: "create",
resourceType: "backup",
resourceId: backup.backupId,
});
} catch (error) {
console.error(error);
throw new TRPCError({
@@ -122,15 +144,42 @@ export const backupRouter = createTRPCRouter({
});
}
}),
one: protectedProcedure.input(apiFindOneBackup).query(async ({ input }) => {
const backup = await findBackupById(input.backupId);
one: protectedProcedure
.input(apiFindOneBackup)
.query(async ({ input, ctx }) => {
const backup = await findBackupById(input.backupId);
return backup;
}),
const serviceId =
backup.postgresId ||
backup.mysqlId ||
backup.mariadbId ||
backup.mongoId ||
backup.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
backup: ["read"],
});
}
return backup;
}),
update: protectedProcedure
.input(apiUpdateBackup)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
const existing = await findBackupById(input.backupId);
const serviceId =
existing.postgresId ||
existing.mysqlId ||
existing.mariadbId ||
existing.mongoId ||
existing.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
backup: ["update"],
});
}
await updateBackupById(input.backupId, input);
const backup = await findBackupById(input.backupId);
@@ -156,6 +205,11 @@ export const backupRouter = createTRPCRouter({
removeScheduleBackup(input.backupId);
}
}
await audit(ctx, {
action: "update",
resourceType: "backup",
resourceId: backup.backupId,
});
} catch (error) {
const message =
error instanceof Error ? error.message : "Error updating this Backup";
@@ -167,8 +221,21 @@ export const backupRouter = createTRPCRouter({
}),
remove: protectedProcedure
.input(apiRemoveBackup)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
const backup = await findBackupById(input.backupId);
const serviceId =
backup.postgresId ||
backup.mysqlId ||
backup.mariadbId ||
backup.mongoId ||
backup.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
backup: ["delete"],
});
}
const value = await removeBackupById(input.backupId);
if (IS_CLOUD && value) {
removeJob({
@@ -179,6 +246,11 @@ export const backupRouter = createTRPCRouter({
} else if (!IS_CLOUD) {
removeScheduleBackup(input.backupId);
}
await audit(ctx, {
action: "delete",
resourceType: "backup",
resourceId: input.backupId,
});
return value;
} catch (error) {
const message =
@@ -191,13 +263,22 @@ export const backupRouter = createTRPCRouter({
}),
manualBackupPostgres: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
const backup = await findBackupById(input.backupId);
if (backup.postgresId) {
await checkServicePermissionAndAccess(ctx, backup.postgresId, {
backup: ["create"],
});
}
const postgres = await findPostgresByBackupId(backup.backupId);
await runPostgresBackup(postgres, backup);
await keepLatestNBackups(backup, postgres?.serverId);
await audit(ctx, {
action: "run",
resourceType: "backup",
resourceId: backup.backupId,
});
return true;
} catch (error) {
const message =
@@ -213,12 +294,22 @@ export const backupRouter = createTRPCRouter({
manualBackupMySql: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
const backup = await findBackupById(input.backupId);
if (backup.mysqlId) {
await checkServicePermissionAndAccess(ctx, backup.mysqlId, {
backup: ["create"],
});
}
const mysql = await findMySqlByBackupId(backup.backupId);
await runMySqlBackup(mysql, backup);
await keepLatestNBackups(backup, mysql?.serverId);
await audit(ctx, {
action: "run",
resourceType: "backup",
resourceId: backup.backupId,
});
return true;
} catch (error) {
throw new TRPCError({
@@ -230,12 +321,22 @@ export const backupRouter = createTRPCRouter({
}),
manualBackupMariadb: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
const backup = await findBackupById(input.backupId);
if (backup.mariadbId) {
await checkServicePermissionAndAccess(ctx, backup.mariadbId, {
backup: ["create"],
});
}
const mariadb = await findMariadbByBackupId(backup.backupId);
await runMariadbBackup(mariadb, backup);
await keepLatestNBackups(backup, mariadb?.serverId);
await audit(ctx, {
action: "run",
resourceType: "backup",
resourceId: backup.backupId,
});
return true;
} catch (error) {
throw new TRPCError({
@@ -247,12 +348,22 @@ export const backupRouter = createTRPCRouter({
}),
manualBackupCompose: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
const backup = await findBackupById(input.backupId);
if (backup.composeId) {
await checkServicePermissionAndAccess(ctx, backup.composeId, {
backup: ["create"],
});
}
const compose = await findComposeByBackupId(backup.backupId);
await runComposeBackup(compose, backup);
await keepLatestNBackups(backup, compose?.serverId);
await audit(ctx, {
action: "run",
resourceType: "backup",
resourceId: backup.backupId,
});
return true;
} catch (error) {
throw new TRPCError({
@@ -264,12 +375,22 @@ export const backupRouter = createTRPCRouter({
}),
manualBackupMongo: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
const backup = await findBackupById(input.backupId);
if (backup.mongoId) {
await checkServicePermissionAndAccess(ctx, backup.mongoId, {
backup: ["create"],
});
}
const mongo = await findMongoByBackupId(backup.backupId);
await runMongoBackup(mongo, backup);
await keepLatestNBackups(backup, mongo?.serverId);
await audit(ctx, {
action: "run",
resourceType: "backup",
resourceId: backup.backupId,
});
return true;
} catch (error) {
throw new TRPCError({
@@ -279,15 +400,20 @@ export const backupRouter = createTRPCRouter({
});
}
}),
manualBackupWebServer: protectedProcedure
manualBackupWebServer: withPermission("backup", "create")
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const backup = await findBackupById(input.backupId);
await runWebServerBackup(backup);
await keepLatestNBackups(backup);
await audit(ctx, {
action: "run",
resourceType: "backup",
resourceId: backup.backupId,
});
return true;
}),
listBackupFiles: protectedProcedure
listBackupFiles: withPermission("backup", "read")
.input(
z.object({
destinationId: z.string(),
@@ -374,7 +500,12 @@ export const backupRouter = createTRPCRouter({
},
})
.input(apiRestoreBackup)
.subscription(async function* ({ input, signal }) {
.subscription(async function* ({ input, ctx, signal }) {
if (input.databaseId) {
await checkServicePermissionAndAccess(ctx, input.databaseId, {
backup: ["restore"],
});
}
const destination = await findDestinationById(input.destinationId);
const queue: string[] = [];
const done = false;

View File

@@ -8,7 +8,12 @@ import {
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
createTRPCRouter,
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiBitbucketTestConnection,
apiCreateBitbucket,
@@ -18,15 +23,23 @@ import {
} from "@/server/db/schema";
export const bitbucketRouter = createTRPCRouter({
create: protectedProcedure
create: withPermission("gitProviders", "create")
.input(apiCreateBitbucket)
.mutation(async ({ input, ctx }) => {
try {
return await createBitbucket(
const result = await createBitbucket(
input,
ctx.session.activeOrganizationId,
ctx.session.userId,
);
await audit(ctx, {
action: "create",
resourceType: "gitProvider",
resourceName: input.name,
});
return result;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -37,19 +50,8 @@ export const bitbucketRouter = createTRPCRouter({
}),
one: protectedProcedure
.input(apiFindOneBitbucket)
.query(async ({ input, ctx }) => {
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
});
}
return bitbucketProvider;
.query(async ({ input }) => {
return await findBitbucketById(input.bitbucketId);
}),
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
let result = await db.query.bitbucket.findMany({
@@ -73,53 +75,18 @@ export const bitbucketRouter = createTRPCRouter({
getBitbucketRepositories: protectedProcedure
.input(apiFindOneBitbucket)
.query(async ({ input, ctx }) => {
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
});
}
.query(async ({ input }) => {
return await getBitbucketRepositories(input.bitbucketId);
}),
getBitbucketBranches: protectedProcedure
.input(apiFindBitbucketBranches)
.query(async ({ input, ctx }) => {
const bitbucketProvider = await findBitbucketById(
input.bitbucketId || "",
);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
});
}
.query(async ({ input }) => {
return await getBitbucketBranches(input);
}),
testConnection: protectedProcedure
.input(apiBitbucketTestConnection)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }) => {
try {
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
});
}
const result = await testBitbucketConnection(input);
return `Found ${result} repositories`;
@@ -130,23 +97,21 @@ export const bitbucketRouter = createTRPCRouter({
});
}
}),
update: protectedProcedure
update: withPermission("gitProviders", "create")
.input(apiUpdateBitbucket)
.mutation(async ({ input, ctx }) => {
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this bitbucket provider",
});
}
return await updateBitbucket(input.bitbucketId, {
const result = await updateBitbucket(input.bitbucketId, {
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "gitProvider",
resourceId: input.bitbucketId,
resourceName: input.name,
});
return result;
}),
});

View File

@@ -7,7 +7,8 @@ import {
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { adminProcedure, createTRPCRouter } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, withPermission } from "@/server/api/trpc";
import {
apiCreateCertificate,
apiFindCertificate,
@@ -15,7 +16,7 @@ import {
} from "@/server/db/schema";
export const certificateRouter = createTRPCRouter({
create: adminProcedure
create: withPermission("certificate", "create")
.input(apiCreateCertificate)
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD && !input.serverId) {
@@ -24,10 +25,20 @@ export const certificateRouter = createTRPCRouter({
message: "Please set a server to create a certificate",
});
}
return await createCertificate(input, ctx.session.activeOrganizationId);
const cert = await createCertificate(
input,
ctx.session.activeOrganizationId,
);
await audit(ctx, {
action: "create",
resourceType: "certificate",
resourceId: cert.certificateId,
resourceName: cert.name,
});
return cert;
}),
one: adminProcedure
one: withPermission("certificate", "read")
.input(apiFindCertificate)
.query(async ({ input, ctx }) => {
const certificates = await findCertificateById(input.certificateId);
@@ -39,7 +50,7 @@ export const certificateRouter = createTRPCRouter({
}
return certificates;
}),
remove: adminProcedure
remove: withPermission("certificate", "delete")
.input(apiFindCertificate)
.mutation(async ({ input, ctx }) => {
const certificates = await findCertificateById(input.certificateId);
@@ -49,10 +60,16 @@ export const certificateRouter = createTRPCRouter({
message: "You are not allowed to delete this certificate",
});
}
await audit(ctx, {
action: "delete",
resourceType: "certificate",
resourceId: certificates.certificateId,
resourceName: certificates.name,
});
await removeCertificateById(input.certificateId);
return true;
}),
all: adminProcedure.query(async ({ ctx }) => {
all: withPermission("certificate", "read").query(async ({ ctx }) => {
return await db.query.certificates.findMany({
where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
});

View File

@@ -7,10 +7,12 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { getLocalServerIp } from "@/server/wss/terminal";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { createTRPCRouter, withPermission } from "../trpc";
export const clusterRouter = createTRPCRouter({
getNodes: protectedProcedure
getNodes: withPermission("server", "read")
.input(
z.object({
serverId: z.string().optional(),
@@ -19,17 +21,17 @@ export const clusterRouter = createTRPCRouter({
.query(async ({ input }) => {
const docker = await getRemoteDocker(input.serverId);
const workers: DockerNode[] = await docker.listNodes();
return workers;
}),
removeWorker: protectedProcedure
removeWorker: withPermission("server", "delete")
.input(
z.object({
nodeId: z.string(),
serverId: z.string().optional(),
}),
)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
const drainCommand = `docker node update --availability drain ${input.nodeId}`;
const removeCommand = `docker node rm ${input.nodeId} --force`;
@@ -41,6 +43,12 @@ export const clusterRouter = createTRPCRouter({
await execAsync(drainCommand);
await execAsync(removeCommand);
}
await audit(ctx, {
action: "delete",
resourceType: "cluster",
resourceId: input.nodeId,
resourceName: input.nodeId,
});
return true;
} catch (error) {
throw new TRPCError({
@@ -50,7 +58,8 @@ export const clusterRouter = createTRPCRouter({
});
}
}),
addWorker: protectedProcedure
addWorker: withPermission("server", "create")
.input(
z.object({
serverId: z.string().optional(),
@@ -68,13 +77,12 @@ export const clusterRouter = createTRPCRouter({
}
return {
command: `docker swarm join --token ${
result.JoinTokens.Worker
} ${ip}:2377`,
command: `docker swarm join --token ${result.JoinTokens.Worker} ${ip}:2377`,
version: docker_version.Version,
};
}),
addManager: protectedProcedure
addManager: withPermission("server", "create")
.input(
z.object({
serverId: z.string().optional(),
@@ -91,9 +99,7 @@ export const clusterRouter = createTRPCRouter({
ip = server?.ipAddress;
}
return {
command: `docker swarm join --token ${
result.JoinTokens.Manager
} ${ip}:2377`,
command: `docker swarm join --token ${result.JoinTokens.Manager} ${ip}:2377`,
version: docker_version.Version,
};
}),

View File

@@ -1,7 +1,5 @@
import {
addDomainToCompose,
addNewService,
checkServiceAccess,
clearOldDeployments,
cloneCompose,
createCommand,
@@ -16,7 +14,6 @@ import {
findDomainsByComposeId,
findEnvironmentById,
findGitProviderById,
findMemberById,
findProjectById,
findServerById,
getComposeContainer,
@@ -34,6 +31,12 @@ import {
updateCompose,
updateDeploymentStatus,
} from "@dokploy/server";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { db } from "@dokploy/server/db";
import {
type CompleteTemplate,
@@ -72,6 +75,7 @@ import {
} from "@/server/queues/queueSetup";
import { cancelDeployment, deploy } from "@/server/utils/deploy";
import { generatePassword } from "@/templates/utils";
import { audit } from "../utils/audit";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
export const composeRouter = createTRPCRouter({
@@ -79,18 +83,10 @@ export const composeRouter = createTRPCRouter({
.input(apiCreateCompose)
.mutation(async ({ ctx, input }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
}
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
@@ -108,14 +104,14 @@ export const composeRouter = createTRPCRouter({
...input,
});
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
newService.composeId,
project.organizationId,
);
}
await addNewService(ctx, newService.composeId);
await audit(ctx, {
action: "create",
resourceType: "service",
resourceId: newService.composeId,
resourceName: newService.appName,
});
return newService;
} catch (error) {
throw error;
@@ -125,14 +121,7 @@ export const composeRouter = createTRPCRouter({
one: protectedProcedure
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.composeId,
ctx.session.activeOrganizationId,
"access",
);
}
await checkServiceAccess(ctx, input.composeId, "read");
const compose = await findComposeById(input.composeId);
if (
@@ -188,29 +177,22 @@ export const composeRouter = createTRPCRouter({
update: protectedProcedure
.input(apiUpdateCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this compose",
});
}
return updateCompose(input.composeId, input);
await checkServicePermissionAndAccess(ctx, input.composeId, {
service: ["create"],
});
const updated = await updateCompose(input.composeId, input);
await audit(ctx, {
action: "update",
resourceType: "compose",
resourceId: input.composeId,
resourceName: updated?.name,
});
return updated;
}),
delete: protectedProcedure
.input(apiDeleteCompose)
.mutation(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.composeId,
ctx.session.activeOrganizationId,
"delete",
);
}
await checkServiceAccess(ctx, input.composeId, "delete");
const composeResult = await findComposeById(input.composeId);
if (
@@ -249,70 +231,55 @@ export const composeRouter = createTRPCRouter({
} catch (_) {}
}
await audit(ctx, {
action: "delete",
resourceType: "service",
resourceId: composeResult.composeId,
resourceName: composeResult.appName,
});
return composeResult;
}),
cleanQueues: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to clean this compose",
});
}
await checkServicePermissionAndAccess(ctx, input.composeId, {
deployment: ["create"],
});
await cleanQueuesByCompose(input.composeId);
return { success: true, message: "Queues cleaned successfully" };
}),
clearDeployments: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
deployment: ["create"],
});
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message:
"You are not authorized to clear deployments for this compose",
});
}
await clearOldDeployments(compose.appName, compose.serverId);
await audit(ctx, {
action: "update",
resourceType: "compose",
resourceId: input.composeId,
resourceName: compose.name,
});
return true;
}),
killBuild: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
deployment: ["cancel"],
});
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to kill this build",
});
}
await killDockerBuild("compose", compose.serverId);
}),
loadServices: protectedProcedure
.input(apiFetchServices)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to load this compose",
});
}
await checkServicePermissionAndAccess(ctx, input.composeId, {
service: ["create"],
});
return await loadServices(input.composeId, input.type);
}),
loadMountsByService: protectedProcedure
@@ -323,16 +290,10 @@ export const composeRouter = createTRPCRouter({
}),
)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
service: ["create"],
});
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to load this compose",
});
}
const container = await getComposeContainer(compose, input.serviceName);
const mounts = container?.Mounts.filter(
(mount) => mount.Type === "volume" && mount.Source !== "",
@@ -343,18 +304,11 @@ export const composeRouter = createTRPCRouter({
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
try {
await checkServicePermissionAndAccess(ctx, input.composeId, {
service: ["create"],
});
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to fetch this compose",
});
}
const command = await cloneCompose(compose);
if (compose.serverId) {
await execAsyncRemote(compose.serverId, command);
@@ -374,49 +328,45 @@ export const composeRouter = createTRPCRouter({
randomizeCompose: protectedProcedure
.input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
service: ["create"],
});
const result = await randomizeComposeFile(input.composeId, input.suffix);
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to randomize this compose",
});
}
return await randomizeComposeFile(input.composeId, input.suffix);
await audit(ctx, {
action: "update",
resourceType: "compose",
resourceId: input.composeId,
resourceName: compose.name,
});
return result;
}),
isolatedDeployment: protectedProcedure
.input(apiRandomizeCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to randomize this compose",
});
}
return await randomizeIsolatedDeploymentComposeFile(
await checkServicePermissionAndAccess(ctx, input.composeId, {
service: ["create"],
});
const result = await randomizeIsolatedDeploymentComposeFile(
input.composeId,
input.suffix,
);
const compose = await findComposeById(input.composeId);
await audit(ctx, {
action: "update",
resourceType: "compose",
resourceId: input.composeId,
resourceName: compose.name,
});
return result;
}),
getConvertedCompose: protectedProcedure
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
service: ["create"],
});
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to get this compose",
});
}
const domains = await findDomainsByComposeId(input.composeId);
const composeFile = await addDomainToCompose(compose, domains);
return stringify(composeFile, {
@@ -427,17 +377,11 @@ export const composeRouter = createTRPCRouter({
deploy: protectedProcedure
.input(apiDeployCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
deployment: ["create"],
});
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this compose",
});
}
const jobData: DeploymentJob = {
composeId: input.composeId,
titleLog: input.title || "Manual deployment",
@@ -452,6 +396,12 @@ export const composeRouter = createTRPCRouter({
deploy(jobData).catch((error) => {
console.error("Background deployment failed:", error);
});
await audit(ctx, {
action: "deploy",
resourceType: "compose",
resourceId: input.composeId,
resourceName: compose.name,
});
return true;
}
await myQueue.add(
@@ -462,6 +412,12 @@ export const composeRouter = createTRPCRouter({
removeOnFail: true,
},
);
await audit(ctx, {
action: "deploy",
resourceType: "compose",
resourceId: input.composeId,
resourceName: compose.name,
});
return {
success: true,
message: "Deployment queued",
@@ -471,16 +427,10 @@ export const composeRouter = createTRPCRouter({
redeploy: protectedProcedure
.input(apiRedeployCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
deployment: ["create"],
});
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to redeploy this compose",
});
}
const jobData: DeploymentJob = {
composeId: input.composeId,
titleLog: input.title || "Rebuild deployment",
@@ -494,6 +444,12 @@ export const composeRouter = createTRPCRouter({
deploy(jobData).catch((error) => {
console.error("Background deployment failed:", error);
});
await audit(ctx, {
action: "deploy",
resourceType: "compose",
resourceId: input.composeId,
resourceName: compose.name,
});
return true;
}
await myQueue.add(
@@ -504,6 +460,12 @@ export const composeRouter = createTRPCRouter({
removeOnFail: true,
},
);
await audit(ctx, {
action: "deploy",
resourceType: "compose",
resourceId: input.composeId,
resourceName: compose.name,
});
return {
success: true,
message: "Redeployment queued",
@@ -513,70 +475,61 @@ export const composeRouter = createTRPCRouter({
stop: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this compose",
});
}
await checkServicePermissionAndAccess(ctx, input.composeId, {
deployment: ["create"],
});
await stopCompose(input.composeId);
const composeForStop = await findComposeById(input.composeId);
await audit(ctx, {
action: "stop",
resourceType: "compose",
resourceId: input.composeId,
resourceName: composeForStop.name,
});
return true;
}),
start: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this compose",
});
}
await checkServicePermissionAndAccess(ctx, input.composeId, {
deployment: ["create"],
});
await startCompose(input.composeId);
const composeForStart = await findComposeById(input.composeId);
await audit(ctx, {
action: "start",
resourceType: "compose",
resourceId: input.composeId,
resourceName: composeForStart.name,
});
return true;
}),
getDefaultCommand: protectedProcedure
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
service: ["create"],
});
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to get this compose",
});
}
const command = createCommand(compose);
return `docker ${command}`;
}),
refreshToken: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to refresh this compose",
});
}
await checkServicePermissionAndAccess(ctx, input.composeId, {
service: ["create"],
});
await updateCompose(input.composeId, {
refreshToken: nanoid(),
});
const composeForToken = await findComposeById(input.composeId);
await audit(ctx, {
action: "update",
resourceType: "compose",
resourceId: input.composeId,
resourceName: composeForToken.name,
});
return true;
}),
deployTemplate: protectedProcedure
@@ -591,14 +544,7 @@ export const composeRouter = createTRPCRouter({
.mutation(async ({ ctx, input }) => {
const environment = await findEnvironmentById(input.environmentId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
environment.projectId,
ctx.session.activeOrganizationId,
"create",
);
}
await checkServiceAccess(ctx, environment.projectId, "create");
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
@@ -648,13 +594,7 @@ export const composeRouter = createTRPCRouter({
isolatedDeployment: true,
});
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
compose.composeId,
ctx.session.activeOrganizationId,
);
}
await addNewService(ctx, compose.composeId);
if (generate.mounts && generate.mounts?.length > 0) {
for (const mount of generate.mounts) {
@@ -681,6 +621,12 @@ export const composeRouter = createTRPCRouter({
}
}
await audit(ctx, {
action: "create",
resourceType: "compose",
resourceId: compose.composeId,
resourceName: compose.name,
});
return compose;
}),
@@ -714,20 +660,11 @@ export const composeRouter = createTRPCRouter({
disconnectGitProvider: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to disconnect this git provider",
});
}
await checkServicePermissionAndAccess(ctx, input.composeId, {
service: ["create"],
});
// Reset all git provider related fields
await updateCompose(input.composeId, {
// GitHub fields
repository: null,
branch: null,
owner: null,
@@ -735,7 +672,6 @@ export const composeRouter = createTRPCRouter({
githubId: null,
triggerType: "push",
// GitLab fields
gitlabRepository: null,
gitlabOwner: null,
gitlabBranch: null,
@@ -743,30 +679,33 @@ export const composeRouter = createTRPCRouter({
gitlabProjectId: null,
gitlabPathNamespace: null,
// Bitbucket fields
bitbucketRepository: null,
bitbucketOwner: null,
bitbucketBranch: null,
bitbucketId: null,
// Gitea fields
giteaRepository: null,
giteaOwner: null,
giteaBranch: null,
giteaId: null,
// Custom Git fields
customGitBranch: null,
customGitUrl: null,
customGitSSHKeyId: null,
// Common fields
sourceType: "github", // Reset to default
composeStatus: "idle",
watchPaths: null,
enableSubmodules: false,
});
const composeForDisconnect = await findComposeById(input.composeId);
await audit(ctx, {
action: "update",
resourceType: "compose",
resourceId: input.composeId,
resourceName: composeForDisconnect.name,
});
return true;
}),
@@ -778,29 +717,9 @@ export const composeRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this compose",
});
}
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
await checkServicePermissionAndAccess(ctx, input.composeId, {
service: ["create"],
});
const updatedCompose = await db
.update(composeTable)
@@ -818,6 +737,12 @@ export const composeRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "compose",
resourceId: input.composeId,
resourceName: updatedCompose.name,
});
return updatedCompose;
}),
@@ -830,18 +755,11 @@ export const composeRouter = createTRPCRouter({
)
.mutation(async ({ input, ctx }) => {
try {
await checkServicePermissionAndAccess(ctx, input.composeId, {
service: ["create"],
});
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this compose",
});
}
const decodedData = Buffer.from(input.base64, "base64").toString(
"utf-8",
);
@@ -901,21 +819,14 @@ export const composeRouter = createTRPCRouter({
)
.mutation(async ({ input, ctx }) => {
try {
await checkServicePermissionAndAccess(ctx, input.composeId, {
service: ["create"],
});
const compose = await findComposeById(input.composeId);
const decodedData = Buffer.from(input.base64, "base64").toString(
"utf-8",
);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this compose",
});
}
for (const mount of compose.mounts) {
await deleteMount(mount.mountId);
}
@@ -993,6 +904,12 @@ export const composeRouter = createTRPCRouter({
}
}
await audit(ctx, {
action: "update",
resourceType: "compose",
resourceId: input.composeId,
resourceName: compose.appName,
});
return {
success: true,
message: "Template imported successfully",
@@ -1008,16 +925,10 @@ export const composeRouter = createTRPCRouter({
cancelDeployment: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.composeId, {
deployment: ["cancel"],
});
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to cancel this deployment",
});
}
if (IS_CLOUD && compose.serverId) {
try {
@@ -1037,6 +948,12 @@ export const composeRouter = createTRPCRouter({
applicationType: "compose",
});
await audit(ctx, {
action: "stop",
resourceType: "compose",
resourceId: input.composeId,
resourceName: compose.name,
});
return {
success: true,
message: "Deployment cancellation requested",
@@ -1113,19 +1030,17 @@ export const composeRouter = createTRPCRouter({
);
}
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${composeTable.composeId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const { accessedServices } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${composeTable.composeId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
const where = and(...baseConditions);

View File

@@ -5,20 +5,21 @@ import {
findAllDeploymentsByComposeId,
findAllDeploymentsByServerId,
findAllDeploymentsCentralized,
findApplicationById,
findComposeById,
findDeploymentById,
findMemberById,
findServerById,
IS_CLOUD,
removeDeployment,
resolveServicePath,
updateDeploymentStatus,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
checkServicePermissionAndAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import {
apiFindAllByApplication,
apiFindAllByCompose,
@@ -29,65 +30,46 @@ import {
} from "@/server/db/schema";
import { myQueue } from "@/server/queues/queueSetup";
import { fetchDeployApiJobs, type QueueJobRow } from "@/server/utils/deploy";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const deploymentRouter = createTRPCRouter({
all: protectedProcedure
.input(apiFindAllByApplication)
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
await checkServicePermissionAndAccess(ctx, input.applicationId, {
deployment: ["read"],
});
return await findAllDeploymentsByApplicationId(input.applicationId);
}),
allByCompose: protectedProcedure
.input(apiFindAllByCompose)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
await checkServicePermissionAndAccess(ctx, input.composeId, {
deployment: ["read"],
});
return await findAllDeploymentsByComposeId(input.composeId);
}),
allByServer: protectedProcedure
allByServer: withPermission("deployment", "read")
.input(apiFindAllByServer)
.query(async ({ input, ctx }) => {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this server",
});
}
.query(async ({ input }) => {
return await findAllDeploymentsByServerId(input.serverId);
}),
allCentralized: protectedProcedure.query(async ({ ctx }) => {
const orgId = ctx.session.activeOrganizationId;
const accessedServices =
ctx.user.role === "member"
? (await findMemberById(ctx.user.id, orgId)).accessedServices
: null;
if (accessedServices !== null && accessedServices.length === 0) {
return [];
}
return findAllDeploymentsCentralized(orgId, accessedServices);
}),
allCentralized: withPermission("deployment", "read").query(
async ({ ctx }) => {
const orgId = ctx.session.activeOrganizationId;
const accessedServices =
ctx.user.role !== "owner" && ctx.user.role !== "admin"
? (await findMemberByUserId(ctx.user.id, orgId)).accessedServices
: null;
if (accessedServices !== null && accessedServices.length === 0) {
return [];
}
return findAllDeploymentsCentralized(orgId, accessedServices);
},
),
queueList: protectedProcedure.query(async ({ ctx }) => {
queueList: withPermission("deployment", "read").query(async ({ ctx }) => {
const orgId = ctx.session.activeOrganizationId;
let rows: QueueJobRow[];
@@ -135,7 +117,10 @@ export const deploymentRouter = createTRPCRouter({
allByType: protectedProcedure
.input(apiFindAllByType)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.id, {
deployment: ["read"],
});
const deploymentsList = await db.query.deployments.findMany({
where: eq(deployments[`${input.type}Id`], input.id),
orderBy: desc(deployments.createdAt),
@@ -151,8 +136,14 @@ export const deploymentRouter = createTRPCRouter({
deploymentId: z.string().min(1),
}),
)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const deployment = await findDeploymentById(input.deploymentId);
const serviceId = deployment.applicationId || deployment.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
deployment: ["cancel"],
});
}
if (!deployment.pid) {
throw new TRPCError({
@@ -169,6 +160,11 @@ export const deploymentRouter = createTRPCRouter({
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await audit(ctx, {
action: "cancel",
resourceType: "deployment",
resourceId: deployment.deploymentId,
});
}),
removeDeployment: protectedProcedure
@@ -177,7 +173,20 @@ export const deploymentRouter = createTRPCRouter({
deploymentId: z.string().min(1),
}),
)
.mutation(async ({ input }) => {
return await removeDeployment(input.deploymentId);
.mutation(async ({ input, ctx }) => {
const deployment = await findDeploymentById(input.deploymentId);
const serviceId = deployment.applicationId || deployment.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
deployment: ["cancel"],
});
}
const result = await removeDeployment(input.deploymentId);
await audit(ctx, {
action: "delete",
resourceType: "deployment",
resourceId: deployment.deploymentId,
});
return result;
}),
});

View File

@@ -10,11 +10,8 @@ import {
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
} from "@/server/api/trpc";
import { createTRPCRouter, withPermission } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateDestination,
apiFindOneDestination,
@@ -24,14 +21,21 @@ import {
} from "@/server/db/schema";
export const destinationRouter = createTRPCRouter({
create: adminProcedure
create: withPermission("destination", "create")
.input(apiCreateDestination)
.mutation(async ({ input, ctx }) => {
try {
return await createDestintation(
const result = await createDestintation(
input,
ctx.session.activeOrganizationId,
);
await audit(ctx, {
action: "create",
resourceType: "destination",
resourceId: result.destinationId,
resourceName: input.name,
});
return result;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -40,7 +44,7 @@ export const destinationRouter = createTRPCRouter({
});
}
}),
testConnection: adminProcedure
testConnection: withPermission("destination", "create")
.input(apiCreateDestination)
.mutation(async ({ input }) => {
const { secretAccessKey, bucket, region, endpoint, accessKey, provider } =
@@ -87,7 +91,7 @@ export const destinationRouter = createTRPCRouter({
});
}
}),
one: protectedProcedure
one: withPermission("destination", "read")
.input(apiFindOneDestination)
.query(async ({ input, ctx }) => {
const destination = await findDestinationById(input.destinationId);
@@ -99,13 +103,13 @@ export const destinationRouter = createTRPCRouter({
}
return destination;
}),
all: protectedProcedure.query(async ({ ctx }) => {
all: withPermission("destination", "read").query(async ({ ctx }) => {
return await db.query.destinations.findMany({
where: eq(destinations.organizationId, ctx.session.activeOrganizationId),
orderBy: [desc(destinations.createdAt)],
});
}),
remove: adminProcedure
remove: withPermission("destination", "delete")
.input(apiRemoveDestination)
.mutation(async ({ input, ctx }) => {
try {
@@ -117,15 +121,22 @@ export const destinationRouter = createTRPCRouter({
message: "You are not allowed to delete this destination",
});
}
return await removeDestinationById(
const result = await removeDestinationById(
input.destinationId,
ctx.session.activeOrganizationId,
);
await audit(ctx, {
action: "delete",
resourceType: "destination",
resourceId: input.destinationId,
resourceName: destination.name,
});
return result;
} catch (error) {
throw error;
}
}),
update: adminProcedure
update: withPermission("destination", "create")
.input(apiUpdateDestination)
.mutation(async ({ input, ctx }) => {
try {
@@ -136,10 +147,17 @@ export const destinationRouter = createTRPCRouter({
message: "You are not allowed to update this destination",
});
}
return await updateDestinationById(input.destinationId, {
const result = await updateDestinationById(input.destinationId, {
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "destination",
resourceId: input.destinationId,
resourceName: input.name,
});
return result;
} catch (error) {
throw error;
}

View File

@@ -10,12 +10,13 @@ import {
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, withPermission } from "../trpc";
export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/;
export const dockerRouter = createTRPCRouter({
getContainers: protectedProcedure
getContainers: withPermission("docker", "read")
.input(
z.object({
serverId: z.string().optional(),
@@ -31,7 +32,7 @@ export const dockerRouter = createTRPCRouter({
return await getContainers(input.serverId);
}),
restartContainer: protectedProcedure
restartContainer: withPermission("docker", "read")
.input(
z.object({
containerId: z
@@ -40,11 +41,18 @@ export const dockerRouter = createTRPCRouter({
.regex(containerIdRegex, "Invalid container id."),
}),
)
.mutation(async ({ input }) => {
return await containerRestart(input.containerId);
.mutation(async ({ input, ctx }) => {
const result = await containerRestart(input.containerId);
await audit(ctx, {
action: "start",
resourceType: "docker",
resourceId: input.containerId,
resourceName: input.containerId,
});
return result;
}),
getConfig: protectedProcedure
getConfig: withPermission("docker", "read")
.input(
z.object({
containerId: z
@@ -64,7 +72,7 @@ export const dockerRouter = createTRPCRouter({
return await getConfig(input.containerId, input.serverId);
}),
getContainersByAppNameMatch: protectedProcedure
getContainersByAppNameMatch: withPermission("service", "read")
.input(
z.object({
appType: z.enum(["stack", "docker-compose"]).optional(),
@@ -86,7 +94,7 @@ export const dockerRouter = createTRPCRouter({
);
}),
getContainersByAppLabel: protectedProcedure
getContainersByAppLabel: withPermission("docker", "read")
.input(
z.object({
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
@@ -108,7 +116,7 @@ export const dockerRouter = createTRPCRouter({
);
}),
getStackContainersByAppName: protectedProcedure
getStackContainersByAppName: withPermission("docker", "read")
.input(
z.object({
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),
@@ -125,7 +133,7 @@ export const dockerRouter = createTRPCRouter({
return await getStackContainersByAppName(input.appName, input.serverId);
}),
getServiceContainersByAppName: protectedProcedure
getServiceContainersByAppName: withPermission("docker", "read")
.input(
z.object({
appName: z.string().min(1).regex(containerIdRegex, "Invalid app name."),

View File

@@ -1,7 +1,6 @@
import {
createDomain,
findApplicationById,
findComposeById,
findDomainById,
findDomainsByApplicationId,
findDomainsByComposeId,
@@ -15,9 +14,15 @@ import {
updateDomainById,
validateDomain,
} from "@dokploy/server";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
createTRPCRouter,
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateDomain,
apiFindCompose,
@@ -32,29 +37,22 @@ export const domainRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
try {
if (input.domainType === "compose" && input.composeId) {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
await checkServicePermissionAndAccess(ctx, input.composeId, {
domain: ["create"],
});
} else if (input.domainType === "application" && input.applicationId) {
const application = await findApplicationById(input.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
await checkServicePermissionAndAccess(ctx, input.applicationId, {
domain: ["create"],
});
}
return await createDomain(input);
const domain = await createDomain(input);
await audit(ctx, {
action: "create",
resourceType: "domain",
resourceId: domain.domainId,
resourceName: domain.host,
});
return domain;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -69,34 +67,20 @@ export const domainRouter = createTRPCRouter({
byApplicationId: protectedProcedure
.input(apiFindOneApplication)
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
await checkServicePermissionAndAccess(ctx, input.applicationId, {
domain: ["read"],
});
return await findDomainsByApplicationId(input.applicationId);
}),
byComposeId: protectedProcedure
.input(apiFindCompose)
.query(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
await checkServicePermissionAndAccess(ctx, input.composeId, {
domain: ["read"],
});
return await findDomainsByComposeId(input.composeId);
}),
generateDomain: protectedProcedure
generateDomain: withPermission("domain", "create")
.input(z.object({ appName: z.string(), serverId: z.string().optional() }))
.mutation(async ({ input, ctx }) => {
return generateTraefikMeDomain(
@@ -105,7 +89,7 @@ export const domainRouter = createTRPCRouter({
input.serverId,
);
}),
canGenerateTraefikMeDomains: protectedProcedure
canGenerateTraefikMeDomains: withPermission("domain", "read")
.input(z.object({ serverId: z.string() }))
.query(async ({ input }) => {
if (input.serverId) {
@@ -120,45 +104,28 @@ export const domainRouter = createTRPCRouter({
.input(apiUpdateDomain)
.mutation(async ({ input, ctx }) => {
const currentDomain = await findDomainById(input.domainId);
if (currentDomain.applicationId) {
const newApp = await findApplicationById(currentDomain.applicationId);
if (
newApp.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
} else if (currentDomain.composeId) {
const newCompose = await findComposeById(currentDomain.composeId);
if (
newCompose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
const serviceId = currentDomain.applicationId || currentDomain.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
domain: ["create"],
});
} else if (currentDomain.previewDeploymentId) {
const newPreviewDeployment = await findPreviewDeploymentById(
const preview = await findPreviewDeploymentById(
currentDomain.previewDeploymentId,
);
if (
newPreviewDeployment.application.environment.project
.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this preview deployment",
});
}
await checkServicePermissionAndAccess(ctx, preview.applicationId, {
domain: ["create"],
});
}
const result = await updateDomainById(input.domainId, input);
const domain = await findDomainById(input.domainId);
await audit(ctx, {
action: "update",
resourceType: "domain",
resourceId: domain.domainId,
resourceName: domain.host,
});
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
await manageDomain(application, domain);
@@ -176,59 +143,46 @@ export const domainRouter = createTRPCRouter({
}),
one: protectedProcedure.input(apiFindDomain).query(async ({ input, ctx }) => {
const domain = await findDomainById(input.domainId);
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
} else if (domain.composeId) {
const compose = await findComposeById(domain.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
const serviceId = domain.applicationId || domain.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
domain: ["read"],
});
} else if (domain.previewDeploymentId) {
const preview = await findPreviewDeploymentById(
domain.previewDeploymentId,
);
await checkServicePermissionAndAccess(ctx, preview.applicationId, {
domain: ["read"],
});
}
return await findDomainById(input.domainId);
return domain;
}),
delete: protectedProcedure
.input(apiFindDomain)
.mutation(async ({ input, ctx }) => {
const domain = await findDomainById(input.domainId);
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
} else if (domain.composeId) {
const compose = await findComposeById(domain.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
const serviceId = domain.applicationId || domain.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
domain: ["delete"],
});
} else if (domain.previewDeploymentId) {
const preview = await findPreviewDeploymentById(
domain.previewDeploymentId,
);
await checkServicePermissionAndAccess(ctx, preview.applicationId, {
domain: ["delete"],
});
}
const result = await removeDomainById(input.domainId);
await audit(ctx, {
action: "delete",
resourceType: "domain",
resourceId: domain.domainId,
resourceName: domain.host,
});
if (domain.applicationId) {
const application = await findApplicationById(domain.applicationId);
@@ -238,7 +192,7 @@ export const domainRouter = createTRPCRouter({
return result;
}),
validateDomain: protectedProcedure
validateDomain: withPermission("domain", "read")
.input(
z.object({
domain: z.string(),

View File

@@ -1,31 +1,35 @@
import {
addNewEnvironment,
checkEnvironmentAccess,
checkEnvironmentCreationPermission,
checkEnvironmentDeletionPermission,
createEnvironment,
deleteEnvironment,
duplicateEnvironment,
findEnvironmentById,
findEnvironmentsByProjectId,
findMemberById,
updateEnvironmentById,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
addNewEnvironment,
checkEnvironmentAccess,
checkEnvironmentCreationPermission,
checkEnvironmentDeletionPermission,
checkPermission,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateEnvironment,
apiDuplicateEnvironment,
apiFindOneEnvironment,
apiRemoveEnvironment,
apiUpdateEnvironment,
environments,
projects,
} from "@/server/db/schema";
import { environments, projects } from "@/server/db/schema";
// Helper function to filter services within an environment based on user permissions
const filterEnvironmentServices = (
environment: any,
accessedServices: string[],
@@ -59,12 +63,7 @@ export const environmentRouter = createTRPCRouter({
.input(apiCreateEnvironment)
.mutation(async ({ input, ctx }) => {
try {
// Check if user has permission to create environments
await checkEnvironmentCreationPermission(
ctx.user.id,
input.projectId,
ctx.session.activeOrganizationId,
);
await checkEnvironmentCreationPermission(ctx, input.projectId);
if (input.name === "production") {
throw new TRPCError({
@@ -74,16 +73,15 @@ export const environmentRouter = createTRPCRouter({
});
}
// Allow users to create environments with any name, including "production"
const environment = await createEnvironment(input);
if (ctx.user.role === "member") {
await addNewEnvironment(
ctx.user.id,
environment.environmentId,
ctx.session.activeOrganizationId,
);
}
await addNewEnvironment(ctx, environment.environmentId);
await audit(ctx, {
action: "create",
resourceType: "environment",
resourceId: environment.environmentId,
resourceName: environment.name,
});
return environment;
} catch (error) {
if (error instanceof TRPCError) {
@@ -100,54 +98,39 @@ export const environmentRouter = createTRPCRouter({
one: protectedProcedure
.input(apiFindOneEnvironment)
.query(async ({ input, ctx }) => {
try {
if (ctx.user.role === "member") {
await checkEnvironmentAccess(
const environment = await findEnvironmentById(input.environmentId);
if (
environment.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not allowed to access this environment",
});
}
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedEnvironments, accessedServices } =
await findMemberByUserId(
ctx.user.id,
input.environmentId,
ctx.session.activeOrganizationId,
"access",
);
}
const environment = await findEnvironmentById(input.environmentId);
if (
environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
if (!accessedEnvironments.includes(environment.environmentId)) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not allowed to access this environment",
});
}
// Check environment access and filter services for members
if (ctx.user.role === "member") {
const { accessedEnvironments, accessedServices } =
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
const filteredEnvironment = filterEnvironmentServices(
environment,
accessedServices,
);
if (!accessedEnvironments.includes(environment.environmentId)) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not allowed to access this environment",
});
}
// Filter services based on member permissions
const filteredEnvironment = filterEnvironmentServices(
environment,
accessedServices,
);
return filteredEnvironment;
}
return environment;
} catch (error) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Environment not found",
});
return filteredEnvironment;
}
return environment;
}),
byProjectId: protectedProcedure
@@ -156,7 +139,6 @@ export const environmentRouter = createTRPCRouter({
try {
const environments = await findEnvironmentsByProjectId(input.projectId);
// Check organization access
if (
environments.some(
(environment) =>
@@ -170,12 +152,13 @@ export const environmentRouter = createTRPCRouter({
});
}
// Filter environments for members based on their permissions
if (ctx.user.role === "member") {
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedEnvironments, accessedServices } =
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
// Filter environments to only show those the member has access to
const filteredEnvironments = environments
.filter((environment) =>
accessedEnvironments.includes(environment.environmentId),
@@ -211,7 +194,6 @@ export const environmentRouter = createTRPCRouter({
});
}
// Prevent deletion of the default environment
if (environment.isDefault) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -219,24 +201,17 @@ export const environmentRouter = createTRPCRouter({
});
}
// Check environment deletion permission
await checkEnvironmentDeletionPermission(
ctx.user.id,
environment.projectId,
ctx.session.activeOrganizationId,
);
await checkEnvironmentDeletionPermission(ctx, environment.projectId);
// Additional check for environment access for members
if (ctx.user.role === "member") {
await checkEnvironmentAccess(
ctx.user.id,
input.environmentId,
ctx.session.activeOrganizationId,
"access",
);
}
await checkEnvironmentAccess(ctx, input.environmentId, "read");
const deletedEnvironment = await deleteEnvironment(input.environmentId);
await audit(ctx, {
action: "delete",
resourceType: "environment",
resourceId: deletedEnvironment?.environmentId,
resourceName: deletedEnvironment?.name,
});
return deletedEnvironment;
} catch (error) {
if (error instanceof TRPCError) {
@@ -256,18 +231,14 @@ export const environmentRouter = createTRPCRouter({
try {
const { environmentId, ...updateData } = input;
// Allow users to rename environments to any name, including "production"
if (ctx.user.role === "member") {
await checkEnvironmentAccess(
ctx.user.id,
environmentId,
ctx.session.activeOrganizationId,
"access",
);
await checkEnvironmentAccess(ctx, environmentId, "read");
if (updateData.env !== undefined) {
await checkPermission(ctx, { environmentEnvVars: ["write"] });
}
const currentEnvironment = await findEnvironmentById(environmentId);
// Prevent renaming the default environment, but allow updating env and description
if (currentEnvironment.isDefault && updateData.name !== undefined) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -284,9 +255,8 @@ export const environmentRouter = createTRPCRouter({
});
}
// Check environment access for members
if (ctx.user.role === "member") {
const { accessedEnvironments } = await findMemberById(
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedEnvironments } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
@@ -305,6 +275,14 @@ export const environmentRouter = createTRPCRouter({
environmentId,
updateData,
);
if (environment) {
await audit(ctx, {
action: "update",
resourceType: "environment",
resourceId: environment.environmentId,
resourceName: environment.name,
});
}
return environment;
} catch (error) {
throw new TRPCError({
@@ -318,14 +296,7 @@ export const environmentRouter = createTRPCRouter({
.input(apiDuplicateEnvironment)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.role === "member") {
await checkEnvironmentAccess(
ctx.user.id,
input.environmentId,
ctx.session.activeOrganizationId,
"access",
);
}
await checkEnvironmentAccess(ctx, input.environmentId, "read");
const environment = await findEnvironmentById(input.environmentId);
if (
environment.project.organizationId !==
@@ -337,9 +308,8 @@ export const environmentRouter = createTRPCRouter({
});
}
// Check environment access for members
if (ctx.user.role === "member") {
const { accessedEnvironments } = await findMemberById(
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedEnvironments } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
@@ -353,6 +323,13 @@ export const environmentRouter = createTRPCRouter({
}
const duplicatedEnvironment = await duplicateEnvironment(input);
await audit(ctx, {
action: "create",
resourceType: "environment",
resourceId: duplicatedEnvironment.environmentId,
resourceName: duplicatedEnvironment.name,
metadata: { duplicatedFrom: input.environmentId },
});
return duplicatedEnvironment;
} catch (error) {
throw new TRPCError({
@@ -404,8 +381,8 @@ export const environmentRouter = createTRPCRouter({
);
}
if (ctx.user.role === "member") {
const { accessedEnvironments } = await findMemberById(
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedEnvironments } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);

View File

@@ -2,7 +2,12 @@ import { findGitProviderById, removeGitProvider } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
createTRPCRouter,
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema";
export const gitProviderRouter = createTRPCRouter({
@@ -21,7 +26,7 @@ export const gitProviderRouter = createTRPCRouter({
),
});
}),
remove: protectedProcedure
remove: withPermission("gitProviders", "delete")
.input(apiRemoveGitProvider)
.mutation(async ({ input, ctx }) => {
try {
@@ -33,6 +38,12 @@ export const gitProviderRouter = createTRPCRouter({
message: "You are not allowed to delete this Git provider",
});
}
await audit(ctx, {
action: "delete",
resourceType: "gitProvider",
resourceId: gitProvider.gitProviderId,
resourceName: gitProvider.name ?? gitProvider.gitProviderId,
});
return await removeGitProvider(input.gitProviderId);
} catch (error) {
const message =

View File

@@ -10,7 +10,12 @@ import {
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
createTRPCRouter,
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateGitea,
apiFindGiteaBranches,
@@ -20,15 +25,24 @@ import {
} from "@/server/db/schema";
export const giteaRouter = createTRPCRouter({
create: protectedProcedure
create: withPermission("gitProviders", "create")
.input(apiCreateGitea)
.mutation(async ({ input, ctx }) => {
try {
return await createGitea(
const result = await createGitea(
input,
ctx.session.activeOrganizationId,
ctx.session.userId,
);
await audit(ctx, {
action: "create",
resourceType: "gitProvider",
resourceId: result.giteaId,
resourceName: input.name,
});
return result;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -38,24 +52,11 @@ export const giteaRouter = createTRPCRouter({
}
}),
one: protectedProcedure
.input(apiFindOneGitea)
.query(async ({ input, ctx }) => {
const giteaProvider = await findGiteaById(input.giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
return giteaProvider;
}),
one: protectedProcedure.input(apiFindOneGitea).query(async ({ input }) => {
return await findGiteaById(input.giteaId);
}),
giteaProviders: protectedProcedure.query(async ({ ctx }: { ctx: any }) => {
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
let result = await db.query.gitea.findMany({
with: {
gitProvider: true,
@@ -85,7 +86,7 @@ export const giteaRouter = createTRPCRouter({
getGiteaRepositories: protectedProcedure
.input(apiFindOneGitea)
.query(async ({ input, ctx }) => {
.query(async ({ input }) => {
const { giteaId } = input;
if (!giteaId) {
@@ -95,18 +96,6 @@ export const giteaRouter = createTRPCRouter({
});
}
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
try {
const repositories = await getGiteaRepositories(giteaId);
return repositories;
@@ -121,7 +110,7 @@ export const giteaRouter = createTRPCRouter({
getGiteaBranches: protectedProcedure
.input(apiFindGiteaBranches)
.query(async ({ input, ctx }) => {
.query(async ({ input }) => {
const { giteaId, owner, repositoryName } = input;
if (!giteaId || !owner || !repositoryName) {
@@ -132,18 +121,6 @@ export const giteaRouter = createTRPCRouter({
});
}
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
try {
return await getGiteaBranches({
giteaId,
@@ -161,22 +138,10 @@ export const giteaRouter = createTRPCRouter({
testConnection: protectedProcedure
.input(apiGiteaTestConnection)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }) => {
const giteaId = input.giteaId ?? "";
try {
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
const result = await testGiteaConnection({
giteaId,
});
@@ -191,21 +156,9 @@ export const giteaRouter = createTRPCRouter({
}
}),
update: protectedProcedure
update: withPermission("gitProviders", "create")
.input(apiUpdateGitea)
.mutation(async ({ input, ctx }) => {
const giteaProvider = await findGiteaById(input.giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
if (input.name) {
await updateGitProvider(input.gitProviderId, {
name: input.name,
@@ -221,12 +174,19 @@ export const giteaRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "gitProvider",
resourceId: input.giteaId,
resourceName: input.name,
});
return { success: true };
}),
getGiteaUrl: protectedProcedure
.input(apiFindOneGitea)
.query(async ({ input, ctx }) => {
.query(async ({ input }) => {
const { giteaId } = input;
if (!giteaId) {
@@ -237,16 +197,6 @@ export const giteaRouter = createTRPCRouter({
}
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitea provider",
});
}
// Return the base URL of the Gitea instance
return giteaProvider.giteaUrl;

View File

@@ -8,7 +8,12 @@ import {
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
createTRPCRouter,
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiFindGithubBranches,
apiFindOneGithub,
@@ -16,53 +21,17 @@ import {
} from "@/server/db/schema";
export const githubRouter = createTRPCRouter({
one: protectedProcedure
.input(apiFindOneGithub)
.query(async ({ input, ctx }) => {
const githubProvider = await findGithubById(input.githubId);
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this github provider",
});
}
return githubProvider;
}),
one: protectedProcedure.input(apiFindOneGithub).query(async ({ input }) => {
return await findGithubById(input.githubId);
}),
getGithubRepositories: protectedProcedure
.input(apiFindOneGithub)
.query(async ({ input, ctx }) => {
const githubProvider = await findGithubById(input.githubId);
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this github provider",
});
}
.query(async ({ input }) => {
return await getGithubRepositories(input.githubId);
}),
getGithubBranches: protectedProcedure
.input(apiFindGithubBranches)
.query(async ({ input, ctx }) => {
const githubProvider = await findGithubById(input.githubId || "");
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this github provider",
});
}
.query(async ({ input }) => {
return await getGithubBranches(input);
}),
githubProviders: protectedProcedure.query(async ({ ctx }) => {
@@ -95,19 +64,8 @@ export const githubRouter = createTRPCRouter({
testConnection: protectedProcedure
.input(apiFindOneGithub)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }) => {
try {
const githubProvider = await findGithubById(input.githubId);
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this github provider",
});
}
const result = await getGithubRepositories(input.githubId);
return `Found ${result.length} repositories`;
} catch (err) {
@@ -117,20 +75,9 @@ export const githubRouter = createTRPCRouter({
});
}
}),
update: protectedProcedure
update: withPermission("gitProviders", "create")
.input(apiUpdateGithub)
.mutation(async ({ input, ctx }) => {
const githubProvider = await findGithubById(input.githubId);
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this github provider",
});
}
await updateGitProvider(input.gitProviderId, {
name: input.name,
organizationId: ctx.session.activeOrganizationId,
@@ -139,5 +86,12 @@ export const githubRouter = createTRPCRouter({
await updateGithub(input.githubId, {
...input,
});
await audit(ctx, {
action: "update",
resourceType: "gitProvider",
resourceId: input.gitProviderId,
resourceName: input.name,
});
}),
});

View File

@@ -10,7 +10,12 @@ import {
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
createTRPCRouter,
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateGitlab,
apiFindGitlabBranches,
@@ -20,15 +25,23 @@ import {
} from "@/server/db/schema";
export const gitlabRouter = createTRPCRouter({
create: protectedProcedure
create: withPermission("gitProviders", "create")
.input(apiCreateGitlab)
.mutation(async ({ input, ctx }) => {
try {
return await createGitlab(
const result = await createGitlab(
input,
ctx.session.activeOrganizationId,
ctx.session.userId,
);
await audit(ctx, {
action: "create",
resourceType: "gitProvider",
resourceName: input.name,
});
return result;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -37,22 +50,9 @@ export const gitlabRouter = createTRPCRouter({
});
}
}),
one: protectedProcedure
.input(apiFindOneGitlab)
.query(async ({ input, ctx }) => {
const gitlabProvider = await findGitlabById(input.gitlabId);
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitlab provider",
});
}
return gitlabProvider;
}),
one: protectedProcedure.input(apiFindOneGitlab).query(async ({ input }) => {
return await findGitlabById(input.gitlabId);
}),
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
let result = await db.query.gitlab.findMany({
with: {
@@ -83,52 +83,19 @@ export const gitlabRouter = createTRPCRouter({
}),
getGitlabRepositories: protectedProcedure
.input(apiFindOneGitlab)
.query(async ({ input, ctx }) => {
const gitlabProvider = await findGitlabById(input.gitlabId);
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitlab provider",
});
}
.query(async ({ input }) => {
return await getGitlabRepositories(input.gitlabId);
}),
getGitlabBranches: protectedProcedure
.input(apiFindGitlabBranches)
.query(async ({ input, ctx }) => {
const gitlabProvider = await findGitlabById(input.gitlabId || "");
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitlab provider",
});
}
.query(async ({ input }) => {
return await getGitlabBranches(input);
}),
testConnection: protectedProcedure
.input(apiGitlabTestConnection)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }) => {
try {
const gitlabProvider = await findGitlabById(input.gitlabId || "");
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitlab provider",
});
}
const result = await testGitlabConnection(input);
return `Found ${result} repositories`;
@@ -139,20 +106,9 @@ export const gitlabRouter = createTRPCRouter({
});
}
}),
update: protectedProcedure
update: withPermission("gitProviders", "create")
.input(apiUpdateGitlab)
.mutation(async ({ input, ctx }) => {
const gitlabProvider = await findGitlabById(input.gitlabId);
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to access this Gitlab provider",
});
}
if (input.name) {
await updateGitProvider(input.gitProviderId, {
name: input.name,
@@ -167,5 +123,12 @@ export const gitlabRouter = createTRPCRouter({
...input,
});
}
await audit(ctx, {
action: "update",
resourceType: "gitProvider",
resourceId: input.gitProviderId,
resourceName: input.name,
});
}),
});

View File

@@ -1,14 +1,11 @@
import {
addNewService,
checkPortInUse,
checkServiceAccess,
createMariadb,
createMount,
deployMariadb,
findBackupsByDbId,
findEnvironmentById,
findMariadbById,
findMemberById,
findProjectById,
IS_CLOUD,
rebuildDatabase,
@@ -21,11 +18,18 @@ import {
updateMariadbById,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiChangeMariaDBStatus,
apiCreateMariaDB,
@@ -36,27 +40,20 @@ import {
apiSaveEnvironmentVariablesMariaDB,
apiSaveExternalPortMariaDB,
apiUpdateMariaDB,
environments,
mariadb as mariadbTable,
projects,
} from "@/server/db/schema";
import { environments, projects } from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
export const mariadbRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMariaDB)
.mutation(async ({ input, ctx }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
}
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
@@ -74,13 +71,7 @@ export const mariadbRouter = createTRPCRouter({
const newMariadb = await createMariadb({
...input,
});
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
newMariadb.mariadbId,
project.organizationId,
);
}
await addNewService(ctx, newMariadb.mariadbId);
await createMount({
serviceId: newMariadb.mariadbId,
@@ -90,6 +81,12 @@ export const mariadbRouter = createTRPCRouter({
type: "volume",
});
await audit(ctx, {
action: "create",
resourceType: "service",
resourceId: newMariadb.mariadbId,
resourceName: newMariadb.appName,
});
return newMariadb;
} catch (error) {
if (error instanceof TRPCError) {
@@ -101,14 +98,7 @@ export const mariadbRouter = createTRPCRouter({
one: protectedProcedure
.input(apiFindOneMariaDB)
.query(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.mariadbId,
ctx.session.activeOrganizationId,
"access",
);
}
await checkServiceAccess(ctx, input.mariadbId, "read");
const mariadb = await findMariadbById(input.mariadbId);
if (
mariadb.environment.project.organizationId !==
@@ -125,16 +115,10 @@ export const mariadbRouter = createTRPCRouter({
start: protectedProcedure
.input(apiFindOneMariaDB)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
deployment: ["create"],
});
const service = await findMariadbById(input.mariadbId);
if (
service.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this Mariadb",
});
}
if (service.serverId) {
await startServiceRemote(service.serverId, service.appName);
} else {
@@ -144,11 +128,20 @@ export const mariadbRouter = createTRPCRouter({
applicationStatus: "done",
});
await audit(ctx, {
action: "start",
resourceType: "service",
resourceId: service.mariadbId,
resourceName: service.appName,
});
return service;
}),
stop: protectedProcedure
.input(apiFindOneMariaDB)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
deployment: ["create"],
});
const mariadb = await findMariadbById(input.mariadbId);
if (mariadb.serverId) {
@@ -160,21 +153,21 @@ export const mariadbRouter = createTRPCRouter({
applicationStatus: "idle",
});
await audit(ctx, {
action: "stop",
resourceType: "service",
resourceId: mariadb.mariadbId,
resourceName: mariadb.appName,
});
return mariadb;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortMariaDB)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
service: ["create"],
});
const mariadb = await findMariadbById(input.mariadbId);
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
});
}
if (input.externalPort) {
const portCheck = await checkPortInUse(
@@ -193,22 +186,28 @@ export const mariadbRouter = createTRPCRouter({
externalPort: input.externalPort,
});
await deployMariadb(input.mariadbId);
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: mariadb.mariadbId,
resourceName: mariadb.appName,
});
return mariadb;
}),
deploy: protectedProcedure
.input(apiDeployMariaDB)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
deployment: ["create"],
});
const mariadb = await findMariadbById(input.mariadbId);
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Mariadb",
});
}
await audit(ctx, {
action: "deploy",
resourceType: "service",
resourceId: mariadb.mariadbId,
resourceName: mariadb.appName,
});
return deployMariadb(input.mariadbId);
}),
deployWithLogs: protectedProcedure
@@ -222,16 +221,9 @@ export const mariadbRouter = createTRPCRouter({
})
.input(apiDeployMariaDB)
.subscription(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Mariadb",
});
}
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
deployment: ["create"],
});
return observable<string>((emit) => {
deployMariadb(input.mariadbId, (log) => {
@@ -242,32 +234,25 @@ export const mariadbRouter = createTRPCRouter({
changeStatus: protectedProcedure
.input(apiChangeMariaDBStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
deployment: ["create"],
});
const mongo = await findMariadbById(input.mariadbId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this Mariadb status",
});
}
await updateMariadbById(input.mariadbId, {
applicationStatus: input.applicationStatus,
});
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: mongo.mariadbId,
resourceName: mongo.appName,
});
return mongo;
}),
remove: protectedProcedure
.input(apiFindOneMariaDB)
.mutation(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.mariadbId,
ctx.session.activeOrganizationId,
"delete",
);
}
await checkServiceAccess(ctx, input.mariadbId, "delete");
const mongo = await findMariadbById(input.mariadbId);
if (
@@ -280,6 +265,12 @@ export const mariadbRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "delete",
resourceType: "service",
resourceId: mongo.mariadbId,
resourceName: mongo.appName,
});
const backups = await findBackupsByDbId(input.mariadbId, "mariadb");
const cleanupOperations = [
async () => await removeService(mongo?.appName, mongo.serverId),
@@ -298,16 +289,9 @@ export const mariadbRouter = createTRPCRouter({
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariablesMariaDB)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
});
}
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
envVars: ["write"],
});
const service = await updateMariadbById(input.mariadbId, {
env: input.env,
});
@@ -319,21 +303,20 @@ export const mariadbRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: input.mariadbId,
});
return true;
}),
reload: protectedProcedure
.input(apiResetMariadb)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
deployment: ["create"],
});
const mariadb = await findMariadbById(input.mariadbId);
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this Mariadb",
});
}
if (mariadb.serverId) {
await stopServiceRemote(mariadb.serverId, mariadb.appName);
} else {
@@ -351,22 +334,21 @@ export const mariadbRouter = createTRPCRouter({
await updateMariadbById(input.mariadbId, {
applicationStatus: "done",
});
await audit(ctx, {
action: "reload",
resourceType: "service",
resourceId: mariadb.mariadbId,
resourceName: mariadb.appName,
});
return true;
}),
update: protectedProcedure
.input(apiUpdateMariaDB)
.mutation(async ({ input, ctx }) => {
const { mariadbId, ...rest } = input;
const mariadb = await findMariadbById(mariadbId);
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this Mariadb",
});
}
await checkServicePermissionAndAccess(ctx, mariadbId, {
service: ["create"],
});
const service = await updateMariadbById(mariadbId, {
...rest,
});
@@ -378,6 +360,12 @@ export const mariadbRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: mariadbId,
resourceName: service.appName,
});
return true;
}),
move: protectedProcedure
@@ -388,31 +376,10 @@ export const mariadbRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mariadb",
});
}
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
service: ["create"],
});
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
// Update the mariadb's projectId
const updatedMariadb = await db
.update(mariadbTable)
.set({
@@ -429,23 +396,27 @@ export const mariadbRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "move",
resourceType: "service",
resourceId: updatedMariadb.mariadbId,
resourceName: updatedMariadb.appName,
});
return updatedMariadb;
}),
rebuild: protectedProcedure
.input(apiRebuildMariadb)
.mutation(async ({ input, ctx }) => {
const mariadb = await findMariadbById(input.mariadbId);
if (
mariadb.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this MariaDB database",
});
}
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
deployment: ["create"],
});
await rebuildDatabase(mariadb.mariadbId, "mariadb");
await rebuildDatabase(input.mariadbId, "mariadb");
await audit(ctx, {
action: "rebuild",
resourceType: "service",
resourceId: input.mariadbId,
});
return true;
}),
search: protectedProcedure
@@ -499,19 +470,18 @@ export const mariadbRouter = createTRPCRouter({
),
);
}
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${mariadbTable.mariadbId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const { accessedServices } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${mariadbTable.mariadbId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db

View File

@@ -1,13 +1,10 @@
import {
addNewService,
checkPortInUse,
checkServiceAccess,
createMongo,
createMount,
deployMongo,
findBackupsByDbId,
findEnvironmentById,
findMemberById,
findMongoById,
findProjectById,
IS_CLOUD,
@@ -20,10 +17,17 @@ import {
stopServiceRemote,
updateMongoById,
} from "@dokploy/server";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangeMongoStatus,
@@ -44,18 +48,10 @@ export const mongoRouter = createTRPCRouter({
.input(apiCreateMongo)
.mutation(async ({ input, ctx }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
}
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
@@ -73,13 +69,7 @@ export const mongoRouter = createTRPCRouter({
const newMongo = await createMongo({
...input,
});
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
newMongo.mongoId,
project.organizationId,
);
}
await addNewService(ctx, newMongo.mongoId);
await createMount({
serviceId: newMongo.mongoId,
@@ -89,6 +79,12 @@ export const mongoRouter = createTRPCRouter({
type: "volume",
});
await audit(ctx, {
action: "create",
resourceType: "service",
resourceId: newMongo.mongoId,
resourceName: newMongo.appName,
});
return newMongo;
} catch (error) {
if (error instanceof TRPCError) {
@@ -104,14 +100,7 @@ export const mongoRouter = createTRPCRouter({
one: protectedProcedure
.input(apiFindOneMongo)
.query(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.mongoId,
ctx.session.activeOrganizationId,
"access",
);
}
await checkServiceAccess(ctx, input.mongoId, "read");
const mongo = await findMongoById(input.mongoId);
if (
@@ -129,18 +118,11 @@ export const mongoRouter = createTRPCRouter({
start: protectedProcedure
.input(apiFindOneMongo)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, {
deployment: ["create"],
});
const service = await findMongoById(input.mongoId);
if (
service.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this mongo",
});
}
if (service.serverId) {
await startServiceRemote(service.serverId, service.appName);
} else {
@@ -150,23 +132,22 @@ export const mongoRouter = createTRPCRouter({
applicationStatus: "done",
});
await audit(ctx, {
action: "start",
resourceType: "service",
resourceId: service.mongoId,
resourceName: service.appName,
});
return service;
}),
stop: protectedProcedure
.input(apiFindOneMongo)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, {
deployment: ["create"],
});
const mongo = await findMongoById(input.mongoId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this mongo",
});
}
if (mongo.serverId) {
await stopServiceRemote(mongo.serverId, mongo.appName);
} else {
@@ -176,21 +157,21 @@ export const mongoRouter = createTRPCRouter({
applicationStatus: "idle",
});
await audit(ctx, {
action: "stop",
resourceType: "service",
resourceId: mongo.mongoId,
resourceName: mongo.appName,
});
return mongo;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortMongo)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, {
service: ["create"],
});
const mongo = await findMongoById(input.mongoId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
});
}
if (input.externalPort) {
const portCheck = await checkPortInUse(
@@ -209,21 +190,27 @@ export const mongoRouter = createTRPCRouter({
externalPort: input.externalPort,
});
await deployMongo(input.mongoId);
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: mongo.mongoId,
resourceName: mongo.appName,
});
return mongo;
}),
deploy: protectedProcedure
.input(apiDeployMongo)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, {
deployment: ["create"],
});
const mongo = await findMongoById(input.mongoId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this mongo",
});
}
await audit(ctx, {
action: "deploy",
resourceType: "service",
resourceId: mongo.mongoId,
resourceName: mongo.appName,
});
return deployMongo(input.mongoId);
}),
deployWithLogs: protectedProcedure
@@ -237,16 +224,9 @@ export const mongoRouter = createTRPCRouter({
})
.input(apiDeployMongo)
.subscription(async function* ({ input, ctx, signal }) {
const mongo = await findMongoById(input.mongoId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this mongo",
});
}
await checkServicePermissionAndAccess(ctx, input.mongoId, {
deployment: ["create"],
});
const queue: string[] = [];
const done = false;
@@ -270,34 +250,28 @@ export const mongoRouter = createTRPCRouter({
changeStatus: protectedProcedure
.input(apiChangeMongoStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, {
deployment: ["create"],
});
const mongo = await findMongoById(input.mongoId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this mongo status",
});
}
await updateMongoById(input.mongoId, {
applicationStatus: input.applicationStatus,
});
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: mongo.mongoId,
resourceName: mongo.appName,
});
return mongo;
}),
reload: protectedProcedure
.input(apiResetMongo)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mongoId, {
deployment: ["create"],
});
const mongo = await findMongoById(input.mongoId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this mongo",
});
}
if (mongo.serverId) {
await stopServiceRemote(mongo.serverId, mongo.appName);
} else {
@@ -315,19 +289,18 @@ export const mongoRouter = createTRPCRouter({
await updateMongoById(input.mongoId, {
applicationStatus: "done",
});
await audit(ctx, {
action: "reload",
resourceType: "service",
resourceId: mongo.mongoId,
resourceName: mongo.appName,
});
return true;
}),
remove: protectedProcedure
.input(apiFindOneMongo)
.mutation(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.mongoId,
ctx.session.activeOrganizationId,
"delete",
);
}
await checkServiceAccess(ctx, input.mongoId, "delete");
const mongo = await findMongoById(input.mongoId);
@@ -340,6 +313,12 @@ export const mongoRouter = createTRPCRouter({
message: "You are not authorized to delete this mongo",
});
}
await audit(ctx, {
action: "delete",
resourceType: "service",
resourceId: mongo.mongoId,
resourceName: mongo.appName,
});
const backups = await findBackupsByDbId(input.mongoId, "mongo");
const cleanupOperations = [
@@ -359,16 +338,9 @@ export const mongoRouter = createTRPCRouter({
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariablesMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
});
}
await checkServicePermissionAndAccess(ctx, input.mongoId, {
envVars: ["write"],
});
const service = await updateMongoById(input.mongoId, {
env: input.env,
});
@@ -380,22 +352,20 @@ export const mongoRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: input.mongoId,
});
return true;
}),
update: protectedProcedure
.input(apiUpdateMongo)
.mutation(async ({ input, ctx }) => {
const { mongoId, ...rest } = input;
const mongo = await findMongoById(mongoId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this mongo",
});
}
await checkServicePermissionAndAccess(ctx, mongoId, {
service: ["create"],
});
const service = await updateMongoById(mongoId, {
...rest,
});
@@ -407,6 +377,12 @@ export const mongoRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: mongoId,
resourceName: service.appName,
});
return true;
}),
move: protectedProcedure
@@ -417,31 +393,10 @@ export const mongoRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mongo",
});
}
await checkServicePermissionAndAccess(ctx, input.mongoId, {
service: ["create"],
});
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
// Update the mongo's projectId
const updatedMongo = await db
.update(mongoTable)
.set({
@@ -458,24 +413,28 @@ export const mongoRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "move",
resourceType: "service",
resourceId: updatedMongo.mongoId,
resourceName: updatedMongo.appName,
});
return updatedMongo;
}),
rebuild: protectedProcedure
.input(apiRebuildMongo)
.mutation(async ({ input, ctx }) => {
const mongo = await findMongoById(input.mongoId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this MongoDB database",
});
}
await checkServicePermissionAndAccess(ctx, input.mongoId, {
deployment: ["create"],
});
await rebuildDatabase(mongo.mongoId, "mongo");
await rebuildDatabase(input.mongoId, "mongo");
await audit(ctx, {
action: "rebuild",
resourceType: "service",
resourceId: input.mongoId,
});
return true;
}),
search: protectedProcedure
@@ -524,19 +483,18 @@ export const mongoRouter = createTRPCRouter({
ilike(mongoTable.description ?? "", `%${input.description.trim()}%`),
);
}
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${mongoTable.mongoId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const { accessedServices } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${mongoTable.mongoId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db

View File

@@ -1,5 +1,4 @@
import {
checkServiceAccess,
createMount,
deleteMount,
findApplicationById,
@@ -7,7 +6,6 @@ import {
findMariadbById,
findMongoById,
findMountById,
findMountOrganizationId,
findMountsByApplicationId,
findMySqlById,
findPostgresById,
@@ -15,6 +13,10 @@ import {
getServiceContainer,
updateMount,
} from "@dokploy/server";
import {
checkServiceAccess,
checkServicePermissionAndAccess,
} from "@dokploy/server/services/permission";
import type { ServiceType } from "@dokploy/server/db/schema/mount";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
@@ -26,6 +28,7 @@ import {
apiUpdateMount,
} from "@/server/db/schema";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { audit } from "@/server/api/utils/audit";
async function getServiceOrganizationId(
serviceId: string,
@@ -68,49 +71,94 @@ async function getServiceOrganizationId(
export const mountRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMount)
.mutation(async ({ input }) => {
return await createMount(input);
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.serviceId, {
volume: ["create"],
});
const mount = await createMount(input);
await audit(ctx, {
action: "create",
resourceType: "mount",
resourceId: mount.mountId,
resourceName: input.mountPath,
});
return mount;
}),
remove: protectedProcedure
.input(apiRemoveMount)
.mutation(async ({ input, ctx }) => {
const organizationId = await findMountOrganizationId(input.mountId);
if (organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this mount",
const mount = await findMountById(input.mountId);
const serviceId =
mount.applicationId ||
mount.postgresId ||
mount.mariadbId ||
mount.mongoId ||
mount.mysqlId ||
mount.redisId ||
mount.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volume: ["delete"],
});
}
await audit(ctx, {
action: "delete",
resourceType: "mount",
resourceId: input.mountId,
});
return await deleteMount(input.mountId);
}),
one: protectedProcedure
.input(apiFindOneMount)
.query(async ({ input, ctx }) => {
const organizationId = await findMountOrganizationId(input.mountId);
if (organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this mount",
const mount = await findMountById(input.mountId);
const serviceId =
mount.applicationId ||
mount.postgresId ||
mount.mariadbId ||
mount.mongoId ||
mount.mysqlId ||
mount.redisId ||
mount.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volume: ["read"],
});
}
return await findMountById(input.mountId);
return mount;
}),
update: protectedProcedure
.input(apiUpdateMount)
.mutation(async ({ input, ctx }) => {
const organizationId = await findMountOrganizationId(input.mountId);
if (organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this mount",
const mount = await findMountById(input.mountId);
const serviceId =
mount.applicationId ||
mount.postgresId ||
mount.mariadbId ||
mount.mongoId ||
mount.mysqlId ||
mount.redisId ||
mount.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volume: ["create"],
});
}
await audit(ctx, {
action: "update",
resourceType: "mount",
resourceId: input.mountId,
resourceName: input.mountPath,
});
return await updateMount(input.mountId, input);
}),
allNamedByApplicationId: protectedProcedure
.input(z.object({ applicationId: z.string().min(1) }))
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.applicationId, {
volume: ["read"],
});
const app = await findApplicationById(input.applicationId);
const container = await getServiceContainer(app.appName, app.serverId);
const mounts = container?.Mounts.filter(
@@ -122,14 +170,7 @@ export const mountRouter = createTRPCRouter({
.input(apiFindMountByApplicationId)
.query(async ({ input, ctx }) => {
console.log("input", input);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.serviceId,
ctx.session.activeOrganizationId,
"access",
);
}
await checkServiceAccess(ctx, input.serviceId, "read");
const organizationId = await getServiceOrganizationId(
input.serviceId,
input.serviceType,

View File

@@ -1,13 +1,10 @@
import {
addNewService,
checkPortInUse,
checkServiceAccess,
createMount,
createMysql,
deployMySql,
findBackupsByDbId,
findEnvironmentById,
findMemberById,
findMySqlById,
findProjectById,
IS_CLOUD,
@@ -20,10 +17,17 @@ import {
stopServiceRemote,
updateMySqlById,
} from "@dokploy/server";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangeMySqlStatus,
@@ -46,18 +50,10 @@ export const mysqlRouter = createTRPCRouter({
.input(apiCreateMySql)
.mutation(async ({ input, ctx }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
}
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
@@ -76,13 +72,7 @@ export const mysqlRouter = createTRPCRouter({
const newMysql = await createMysql({
...input,
});
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
newMysql.mysqlId,
project.organizationId,
);
}
await addNewService(ctx, newMysql.mysqlId);
await createMount({
serviceId: newMysql.mysqlId,
@@ -92,6 +82,12 @@ export const mysqlRouter = createTRPCRouter({
type: "volume",
});
await audit(ctx, {
action: "create",
resourceType: "service",
resourceId: newMysql.mysqlId,
resourceName: newMysql.appName,
});
return newMysql;
} catch (error) {
if (error instanceof TRPCError) {
@@ -107,14 +103,7 @@ export const mysqlRouter = createTRPCRouter({
one: protectedProcedure
.input(apiFindOneMySql)
.query(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.mysqlId,
ctx.session.activeOrganizationId,
"access",
);
}
await checkServiceAccess(ctx, input.mysqlId, "read");
const mysql = await findMySqlById(input.mysqlId);
if (
mysql.environment.project.organizationId !==
@@ -131,16 +120,10 @@ export const mysqlRouter = createTRPCRouter({
start: protectedProcedure
.input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
deployment: ["create"],
});
const service = await findMySqlById(input.mysqlId);
if (
service.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this MySQL",
});
}
if (service.serverId) {
await startServiceRemote(service.serverId, service.appName);
@@ -151,21 +134,21 @@ export const mysqlRouter = createTRPCRouter({
applicationStatus: "done",
});
await audit(ctx, {
action: "start",
resourceType: "service",
resourceId: service.mysqlId,
resourceName: service.appName,
});
return service;
}),
stop: protectedProcedure
.input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
deployment: ["create"],
});
const mongo = await findMySqlById(input.mysqlId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this MySQL",
});
}
if (mongo.serverId) {
await stopServiceRemote(mongo.serverId, mongo.appName);
} else {
@@ -175,21 +158,21 @@ export const mysqlRouter = createTRPCRouter({
applicationStatus: "idle",
});
await audit(ctx, {
action: "stop",
resourceType: "service",
resourceId: mongo.mysqlId,
resourceName: mongo.appName,
});
return mongo;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortMySql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
service: ["create"],
});
const mysql = await findMySqlById(input.mysqlId);
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
});
}
if (input.externalPort) {
const portCheck = await checkPortInUse(
@@ -208,21 +191,27 @@ export const mysqlRouter = createTRPCRouter({
externalPort: input.externalPort,
});
await deployMySql(input.mysqlId);
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: mysql.mysqlId,
resourceName: mysql.appName,
});
return mysql;
}),
deploy: protectedProcedure
.input(apiDeployMySql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
deployment: ["create"],
});
const mysql = await findMySqlById(input.mysqlId);
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this MySQL",
});
}
await audit(ctx, {
action: "deploy",
resourceType: "service",
resourceId: mysql.mysqlId,
resourceName: mysql.appName,
});
return deployMySql(input.mysqlId);
}),
deployWithLogs: protectedProcedure
@@ -236,16 +225,9 @@ export const mysqlRouter = createTRPCRouter({
})
.input(apiDeployMySql)
.subscription(async function* ({ input, ctx, signal }) {
const mysql = await findMySqlById(input.mysqlId);
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this MySQL",
});
}
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
deployment: ["create"],
});
const queue: string[] = [];
const done = false;
@@ -269,34 +251,28 @@ export const mysqlRouter = createTRPCRouter({
changeStatus: protectedProcedure
.input(apiChangeMySqlStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
deployment: ["create"],
});
const mongo = await findMySqlById(input.mysqlId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this MySQL status",
});
}
await updateMySqlById(input.mysqlId, {
applicationStatus: input.applicationStatus,
});
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: mongo.mysqlId,
resourceName: mongo.appName,
});
return mongo;
}),
reload: protectedProcedure
.input(apiResetMysql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
deployment: ["create"],
});
const mysql = await findMySqlById(input.mysqlId);
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this MySQL",
});
}
if (mysql.serverId) {
await stopServiceRemote(mysql.serverId, mysql.appName);
} else {
@@ -313,19 +289,18 @@ export const mysqlRouter = createTRPCRouter({
await updateMySqlById(input.mysqlId, {
applicationStatus: "done",
});
await audit(ctx, {
action: "reload",
resourceType: "service",
resourceId: mysql.mysqlId,
resourceName: mysql.appName,
});
return true;
}),
remove: protectedProcedure
.input(apiFindOneMySql)
.mutation(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.mysqlId,
ctx.session.activeOrganizationId,
"delete",
);
}
await checkServiceAccess(ctx, input.mysqlId, "delete");
const mongo = await findMySqlById(input.mysqlId);
if (
mongo.environment.project.organizationId !==
@@ -337,6 +312,12 @@ export const mysqlRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "delete",
resourceType: "service",
resourceId: mongo.mysqlId,
resourceName: mongo.appName,
});
const backups = await findBackupsByDbId(input.mysqlId, "mysql");
const cleanupOperations = [
async () => await removeService(mongo?.appName, mongo.serverId),
@@ -355,16 +336,9 @@ export const mysqlRouter = createTRPCRouter({
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariablesMySql)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
});
}
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
envVars: ["write"],
});
const service = await updateMySqlById(input.mysqlId, {
env: input.env,
});
@@ -376,22 +350,20 @@ export const mysqlRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: input.mysqlId,
});
return true;
}),
update: protectedProcedure
.input(apiUpdateMySql)
.mutation(async ({ input, ctx }) => {
const { mysqlId, ...rest } = input;
const mysql = await findMySqlById(mysqlId);
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this MySQL",
});
}
await checkServicePermissionAndAccess(ctx, mysqlId, {
service: ["create"],
});
const service = await updateMySqlById(mysqlId, {
...rest,
});
@@ -403,6 +375,12 @@ export const mysqlRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: mysqlId,
resourceName: service.appName,
});
return true;
}),
move: protectedProcedure
@@ -413,31 +391,10 @@ export const mysqlRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this mysql",
});
}
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
service: ["create"],
});
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
// Update the mysql's projectId
const updatedMysql = await db
.update(mysqlTable)
.set({
@@ -454,24 +411,28 @@ export const mysqlRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "move",
resourceType: "service",
resourceId: updatedMysql.mysqlId,
resourceName: updatedMysql.appName,
});
return updatedMysql;
}),
rebuild: protectedProcedure
.input(apiRebuildMysql)
.mutation(async ({ input, ctx }) => {
const mysql = await findMySqlById(input.mysqlId);
if (
mysql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this MySQL database",
});
}
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
deployment: ["create"],
});
await rebuildDatabase(mysql.mysqlId, "mysql");
await rebuildDatabase(input.mysqlId, "mysql");
await audit(ctx, {
action: "rebuild",
resourceType: "service",
resourceId: input.mysqlId,
});
return true;
}),
search: protectedProcedure
@@ -520,19 +481,18 @@ export const mysqlRouter = createTRPCRouter({
ilike(mysqlTable.description ?? "", `%${input.description.trim()}%`),
);
}
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${mysqlTable.mysqlId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const { accessedServices } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${mysqlTable.mysqlId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db

View File

@@ -43,11 +43,11 @@ import { TRPCError } from "@trpc/server";
import { desc, eq, sql } from "drizzle-orm";
import { z } from "zod";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
publicProcedure,
withPermission,
} from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateCustom,
apiCreateDiscord,
@@ -88,15 +88,18 @@ import {
} from "@/server/db/schema";
export const notificationRouter = createTRPCRouter({
createSlack: adminProcedure
createSlack: withPermission("notification", "create")
.input(apiCreateSlack)
.mutation(async ({ input, ctx }) => {
try {
return await createSlackNotification(
input,
ctx.session.activeOrganizationId,
);
await createSlackNotification(input, ctx.session.activeOrganizationId);
await audit(ctx, {
action: "create",
resourceType: "notification",
resourceName: input.name,
});
} catch (error) {
console.log(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the notification",
@@ -104,7 +107,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateSlack: adminProcedure
updateSlack: withPermission("notification", "update")
.input(apiUpdateSlack)
.mutation(async ({ input, ctx }) => {
try {
@@ -115,15 +118,22 @@ export const notificationRouter = createTRPCRouter({
message: "You are not authorized to update this notification",
});
}
return await updateSlackNotification({
const result = await updateSlackNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "notification",
resourceId: input.notificationId,
resourceName: notification.name,
});
return result;
} catch (error) {
throw error;
}
}),
testSlackConnection: adminProcedure
testSlackConnection: withPermission("notification", "create")
.input(apiTestSlackConnection)
.mutation(async ({ input }) => {
try {
@@ -140,14 +150,19 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
createTelegram: adminProcedure
createTelegram: withPermission("notification", "create")
.input(apiCreateTelegram)
.mutation(async ({ input, ctx }) => {
try {
return await createTelegramNotification(
await createTelegramNotification(
input,
ctx.session.activeOrganizationId,
);
await audit(ctx, {
action: "create",
resourceType: "notification",
resourceName: input.name,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -157,7 +172,7 @@ export const notificationRouter = createTRPCRouter({
}
}),
updateTelegram: adminProcedure
updateTelegram: withPermission("notification", "update")
.input(apiUpdateTelegram)
.mutation(async ({ input, ctx }) => {
try {
@@ -168,10 +183,17 @@ export const notificationRouter = createTRPCRouter({
message: "You are not authorized to update this notification",
});
}
return await updateTelegramNotification({
const result = await updateTelegramNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "notification",
resourceId: input.notificationId,
resourceName: notification.name,
});
return result;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -180,7 +202,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
testTelegramConnection: adminProcedure
testTelegramConnection: withPermission("notification", "create")
.input(apiTestTelegramConnection)
.mutation(async ({ input }) => {
try {
@@ -194,14 +216,19 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
createDiscord: adminProcedure
createDiscord: withPermission("notification", "create")
.input(apiCreateDiscord)
.mutation(async ({ input, ctx }) => {
try {
return await createDiscordNotification(
await createDiscordNotification(
input,
ctx.session.activeOrganizationId,
);
await audit(ctx, {
action: "create",
resourceType: "notification",
resourceName: input.name,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -211,7 +238,7 @@ export const notificationRouter = createTRPCRouter({
}
}),
updateDiscord: adminProcedure
updateDiscord: withPermission("notification", "update")
.input(apiUpdateDiscord)
.mutation(async ({ input, ctx }) => {
try {
@@ -222,10 +249,17 @@ export const notificationRouter = createTRPCRouter({
message: "You are not authorized to update this notification",
});
}
return await updateDiscordNotification({
const result = await updateDiscordNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "notification",
resourceId: input.notificationId,
resourceName: notification.name,
});
return result;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -235,7 +269,7 @@ export const notificationRouter = createTRPCRouter({
}
}),
testDiscordConnection: adminProcedure
testDiscordConnection: withPermission("notification", "create")
.input(apiTestDiscordConnection)
.mutation(async ({ input }) => {
try {
@@ -257,14 +291,16 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
createEmail: adminProcedure
createEmail: withPermission("notification", "create")
.input(apiCreateEmail)
.mutation(async ({ input, ctx }) => {
try {
return await createEmailNotification(
input,
ctx.session.activeOrganizationId,
);
await createEmailNotification(input, ctx.session.activeOrganizationId);
await audit(ctx, {
action: "create",
resourceType: "notification",
resourceName: input.name,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -273,7 +309,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateEmail: adminProcedure
updateEmail: withPermission("notification", "update")
.input(apiUpdateEmail)
.mutation(async ({ input, ctx }) => {
try {
@@ -284,10 +320,17 @@ export const notificationRouter = createTRPCRouter({
message: "You are not authorized to update this notification",
});
}
return await updateEmailNotification({
const result = await updateEmailNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "notification",
resourceId: input.notificationId,
resourceName: notification.name,
});
return result;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -296,7 +339,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
testEmailConnection: adminProcedure
testEmailConnection: withPermission("notification", "create")
.input(apiTestEmailConnection)
.mutation(async ({ input }) => {
try {
@@ -314,14 +357,16 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
createResend: adminProcedure
createResend: withPermission("notification", "create")
.input(apiCreateResend)
.mutation(async ({ input, ctx }) => {
try {
return await createResendNotification(
input,
ctx.session.activeOrganizationId,
);
await createResendNotification(input, ctx.session.activeOrganizationId);
await audit(ctx, {
action: "create",
resourceType: "notification",
resourceName: input.name,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -330,7 +375,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateResend: adminProcedure
updateResend: withPermission("notification", "update")
.input(apiUpdateResend)
.mutation(async ({ input, ctx }) => {
try {
@@ -341,10 +386,17 @@ export const notificationRouter = createTRPCRouter({
message: "You are not authorized to update this notification",
});
}
return await updateResendNotification({
const result = await updateResendNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "notification",
resourceId: input.notificationId,
resourceName: notification.name,
});
return result;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -353,7 +405,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
testResendConnection: adminProcedure
testResendConnection: withPermission("notification", "create")
.input(apiTestResendConnection)
.mutation(async ({ input }) => {
try {
@@ -371,7 +423,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
remove: adminProcedure
remove: withPermission("notification", "delete")
.input(apiFindOneNotification)
.mutation(async ({ input, ctx }) => {
try {
@@ -382,6 +434,11 @@ export const notificationRouter = createTRPCRouter({
message: "You are not authorized to delete this notification",
});
}
await audit(ctx, {
action: "delete",
resourceType: "notification",
resourceName: notification.name,
});
return await removeNotificationById(input.notificationId);
} catch (error) {
const message =
@@ -394,7 +451,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
one: protectedProcedure
one: withPermission("notification", "read")
.input(apiFindOneNotification)
.query(async ({ input, ctx }) => {
const notification = await findNotificationById(input.notificationId);
@@ -406,7 +463,7 @@ export const notificationRouter = createTRPCRouter({
}
return notification;
}),
all: adminProcedure.query(async ({ ctx }) => {
all: withPermission("notification", "read").query(async ({ ctx }) => {
return await db.query.notifications.findMany({
with: {
slack: true,
@@ -453,8 +510,6 @@ export const notificationRouter = createTRPCRouter({
});
}
// For Dokploy server type, we don't have a specific organizationId
// This might need to be adjusted based on your business logic
organizationId = "";
ServerName = "Dokploy";
} else {
@@ -488,14 +543,16 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
createGotify: adminProcedure
createGotify: withPermission("notification", "create")
.input(apiCreateGotify)
.mutation(async ({ input, ctx }) => {
try {
return await createGotifyNotification(
input,
ctx.session.activeOrganizationId,
);
await createGotifyNotification(input, ctx.session.activeOrganizationId);
await audit(ctx, {
action: "create",
resourceType: "notification",
resourceName: input.name,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -504,7 +561,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateGotify: adminProcedure
updateGotify: withPermission("notification", "update")
.input(apiUpdateGotify)
.mutation(async ({ input, ctx }) => {
try {
@@ -518,15 +575,22 @@ export const notificationRouter = createTRPCRouter({
message: "You are not authorized to update this notification",
});
}
return await updateGotifyNotification({
const result = await updateGotifyNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "notification",
resourceId: input.notificationId,
resourceName: notification.name,
});
return result;
} catch (error) {
throw error;
}
}),
testGotifyConnection: adminProcedure
testGotifyConnection: withPermission("notification", "create")
.input(apiTestGotifyConnection)
.mutation(async ({ input }) => {
try {
@@ -544,14 +608,16 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
createNtfy: adminProcedure
createNtfy: withPermission("notification", "create")
.input(apiCreateNtfy)
.mutation(async ({ input, ctx }) => {
try {
return await createNtfyNotification(
input,
ctx.session.activeOrganizationId,
);
await createNtfyNotification(input, ctx.session.activeOrganizationId);
await audit(ctx, {
action: "create",
resourceType: "notification",
resourceName: input.name,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -560,7 +626,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateNtfy: adminProcedure
updateNtfy: withPermission("notification", "update")
.input(apiUpdateNtfy)
.mutation(async ({ input, ctx }) => {
try {
@@ -574,15 +640,22 @@ export const notificationRouter = createTRPCRouter({
message: "You are not authorized to update this notification",
});
}
return await updateNtfyNotification({
const result = await updateNtfyNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "notification",
resourceId: input.notificationId,
resourceName: notification.name,
});
return result;
} catch (error) {
throw error;
}
}),
testNtfyConnection: adminProcedure
testNtfyConnection: withPermission("notification", "create")
.input(apiTestNtfyConnection)
.mutation(async ({ input }) => {
try {
@@ -602,14 +675,16 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
createCustom: adminProcedure
createCustom: withPermission("notification", "create")
.input(apiCreateCustom)
.mutation(async ({ input, ctx }) => {
try {
return await createCustomNotification(
input,
ctx.session.activeOrganizationId,
);
await createCustomNotification(input, ctx.session.activeOrganizationId);
await audit(ctx, {
action: "create",
resourceType: "notification",
resourceName: input.name,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -618,7 +693,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateCustom: adminProcedure
updateCustom: withPermission("notification", "update")
.input(apiUpdateCustom)
.mutation(async ({ input, ctx }) => {
try {
@@ -629,15 +704,22 @@ export const notificationRouter = createTRPCRouter({
message: "You are not authorized to update this notification",
});
}
return await updateCustomNotification({
const result = await updateCustomNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "notification",
resourceId: input.notificationId,
resourceName: notification.name,
});
return result;
} catch (error) {
throw error;
}
}),
testCustomConnection: adminProcedure
testCustomConnection: withPermission("notification", "create")
.input(apiTestCustomConnection)
.mutation(async ({ input }) => {
try {
@@ -655,14 +737,16 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
createLark: adminProcedure
createLark: withPermission("notification", "create")
.input(apiCreateLark)
.mutation(async ({ input, ctx }) => {
try {
return await createLarkNotification(
input,
ctx.session.activeOrganizationId,
);
await createLarkNotification(input, ctx.session.activeOrganizationId);
await audit(ctx, {
action: "create",
resourceType: "notification",
resourceName: input.name,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -671,7 +755,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateLark: adminProcedure
updateLark: withPermission("notification", "update")
.input(apiUpdateLark)
.mutation(async ({ input, ctx }) => {
try {
@@ -685,15 +769,22 @@ export const notificationRouter = createTRPCRouter({
message: "You are not authorized to update this notification",
});
}
return await updateLarkNotification({
const result = await updateLarkNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "notification",
resourceId: input.notificationId,
resourceName: notification.name,
});
return result;
} catch (error) {
throw error;
}
}),
testLarkConnection: adminProcedure
testLarkConnection: withPermission("notification", "create")
.input(apiTestLarkConnection)
.mutation(async ({ input }) => {
try {
@@ -712,14 +803,16 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
createTeams: adminProcedure
createTeams: withPermission("notification", "create")
.input(apiCreateTeams)
.mutation(async ({ input, ctx }) => {
try {
return await createTeamsNotification(
input,
ctx.session.activeOrganizationId,
);
await createTeamsNotification(input, ctx.session.activeOrganizationId);
await audit(ctx, {
action: "create",
resourceType: "notification",
resourceName: input.name,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -728,7 +821,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateTeams: adminProcedure
updateTeams: withPermission("notification", "update")
.input(apiUpdateTeams)
.mutation(async ({ input, ctx }) => {
try {
@@ -742,15 +835,22 @@ export const notificationRouter = createTRPCRouter({
message: "You are not authorized to update this notification",
});
}
return await updateTeamsNotification({
const result = await updateTeamsNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "notification",
resourceId: input.notificationId,
resourceName: notification.name,
});
return result;
} catch (error) {
throw error;
}
}),
testTeamsConnection: adminProcedure
testTeamsConnection: withPermission("notification", "create")
.input(apiTestTeamsConnection)
.mutation(async ({ input }) => {
try {
@@ -767,14 +867,19 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
createPushover: adminProcedure
createPushover: withPermission("notification", "create")
.input(apiCreatePushover)
.mutation(async ({ input, ctx }) => {
try {
return await createPushoverNotification(
await createPushoverNotification(
input,
ctx.session.activeOrganizationId,
);
await audit(ctx, {
action: "create",
resourceType: "notification",
resourceName: input.name,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -783,7 +888,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updatePushover: adminProcedure
updatePushover: withPermission("notification", "update")
.input(apiUpdatePushover)
.mutation(async ({ input, ctx }) => {
try {
@@ -797,15 +902,22 @@ export const notificationRouter = createTRPCRouter({
message: "You are not authorized to update this notification",
});
}
return await updatePushoverNotification({
const result = await updatePushoverNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "notification",
resourceId: input.notificationId,
resourceName: notification.name,
});
return result;
} catch (error) {
throw error;
}
}),
testPushoverConnection: adminProcedure
testPushoverConnection: withPermission("notification", "create")
.input(apiTestPushoverConnection)
.mutation(async ({ input }) => {
try {
@@ -823,13 +935,18 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
return await db.query.notifications.findMany({
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
with: {
email: true,
resend: true,
},
});
}),
getEmailProviders: withPermission("notification", "read").query(
async ({ ctx }) => {
return await db.query.notifications.findMany({
where: eq(
notifications.organizationId,
ctx.session.activeOrganizationId,
),
with: {
email: true,
resend: true,
},
});
},
),
});

View File

@@ -1,11 +1,18 @@
import { db } from "@dokploy/server/db";
import { IS_CLOUD } from "@dokploy/server/index";
import { audit } from "@/server/api/utils/audit";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, exists } from "drizzle-orm";
import { nanoid } from "nanoid";
import { z } from "zod";
import { invitation, member, organization } from "@/server/db/schema";
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
import {
invitation,
member,
organization,
organizationRole,
user,
} from "@/server/db/schema";
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const organizationRouter = createTRPCRouter({
create: protectedProcedure
.input(
@@ -50,6 +57,12 @@ export const organizationRouter = createTRPCRouter({
createdAt: new Date(),
userId: ctx.user.id,
});
await audit(ctx, {
action: "create",
resourceType: "organization",
resourceId: result.id,
resourceName: result.name,
});
return result;
}),
all: protectedProcedure.query(async ({ ctx }) => {
@@ -156,6 +169,12 @@ export const organizationRouter = createTRPCRouter({
})
.where(eq(organization.id, input.organizationId))
.returning();
await audit(ctx, {
action: "update",
resourceType: "organization",
resourceId: input.organizationId,
resourceName: input.name,
});
return result[0];
}),
delete: protectedProcedure
@@ -220,15 +239,109 @@ export const organizationRouter = createTRPCRouter({
.delete(organization)
.where(eq(organization.id, input.organizationId));
await audit(ctx, {
action: "delete",
resourceType: "organization",
resourceId: input.organizationId,
resourceName: org.name,
});
return result;
}),
allInvitations: adminProcedure.query(async ({ ctx }) => {
inviteMember: withPermission("member", "create")
.input(
z.object({
email: z.string().email(),
role: z.string().min(1),
}),
)
.mutation(async ({ ctx, input }) => {
const orgId = ctx.session.activeOrganizationId;
const email = input.email.toLowerCase();
// Check if user is already a member
const existingUser = await db.query.user.findFirst({
where: eq(user.email, email),
});
if (existingUser) {
const existingMember = await db.query.member.findFirst({
where: and(
eq(member.organizationId, orgId),
eq(member.userId, existingUser.id),
),
});
if (existingMember) {
throw new TRPCError({
code: "CONFLICT",
message: "User is already a member of this organization",
});
}
}
// Check for pending invitation
const existingInvitation = await db.query.invitation.findFirst({
where: and(
eq(invitation.organizationId, orgId),
eq(invitation.email, email),
eq(invitation.status, "pending"),
),
});
if (existingInvitation) {
throw new TRPCError({
code: "CONFLICT",
message: "An invitation has already been sent to this email",
});
}
// If assigning a custom role, verify it exists
if (!["owner", "admin", "member"].includes(input.role)) {
const customRole = await db.query.organizationRole.findFirst({
where: and(
eq(organizationRole.organizationId, orgId),
eq(organizationRole.role, input.role),
),
});
if (!customRole) {
throw new TRPCError({
code: "NOT_FOUND",
message: `Role "${input.role}" not found`,
});
}
}
const [created] = await db
.insert(invitation)
.values({
id: nanoid(),
organizationId: orgId,
email,
role: input.role as any,
status: "pending",
expiresAt: new Date(Date.now() + 48 * 60 * 60 * 1000),
inviterId: ctx.user.id,
})
.returning();
await audit(ctx, {
action: "create",
resourceType: "organization",
resourceId: created?.id,
resourceName: email,
metadata: { type: "inviteMember", role: input.role },
});
return created;
}),
allInvitations: withPermission("member", "create").query(async ({ ctx }) => {
return await db.query.invitation.findMany({
where: eq(invitation.organizationId, ctx.session.activeOrganizationId),
orderBy: [desc(invitation.status), desc(invitation.expiresAt)],
});
}),
removeInvitation: adminProcedure
removeInvitation: withPermission("member", "create")
.input(z.object({ invitationId: z.string() }))
.mutation(async ({ ctx, input }) => {
const invitationResult = await db.query.invitation.findFirst({
@@ -251,15 +364,23 @@ export const organizationRouter = createTRPCRouter({
});
}
return await db
const result = await db
.delete(invitation)
.where(eq(invitation.id, input.invitationId));
await audit(ctx, {
action: "delete",
resourceType: "organization",
resourceId: input.invitationId,
resourceName: invitationResult.email,
metadata: { type: "removeInvitation" },
});
return result;
}),
updateMemberRole: adminProcedure
updateMemberRole: withPermission("member", "update")
.input(
z.object({
memberId: z.string(),
role: z.enum(["admin", "member"]),
role: z.string().min(1),
}),
)
.mutation(async ({ ctx, input }) => {
@@ -289,7 +410,7 @@ export const organizationRouter = createTRPCRouter({
}
// Owner role is intransferible - cannot change to or from owner
if (target.role === "owner") {
if (target.role === "owner" || input.role === "owner") {
throw new TRPCError({
code: "FORBIDDEN",
message: "The owner role is intransferible",
@@ -306,12 +427,39 @@ export const organizationRouter = createTRPCRouter({
});
}
// If assigning a custom role (not admin/member), verify it exists
if (input.role !== "admin" && input.role !== "member") {
const customRole = await db.query.organizationRole.findFirst({
where: and(
eq(
organizationRole.organizationId,
ctx.session.activeOrganizationId,
),
eq(organizationRole.role, input.role),
),
});
if (!customRole) {
throw new TRPCError({
code: "NOT_FOUND",
message: `Custom role "${input.role}" not found`,
});
}
}
// Update the target member's role
await db
.update(member)
.set({ role: input.role })
.where(eq(member.id, input.memberId));
await audit(ctx, {
action: "update",
resourceType: "user",
resourceId: target.userId,
resourceName: target.user.email,
metadata: { before: target.role, after: input.role },
});
return true;
}),
setDefault: protectedProcedure
@@ -353,6 +501,12 @@ export const organizationRouter = createTRPCRouter({
),
);
await audit(ctx, {
action: "update",
resourceType: "organization",
resourceId: input.organizationId,
metadata: { type: "setDefault" },
});
return { success: true };
}),
active: protectedProcedure.query(async ({ ctx }) => {

View File

@@ -1,5 +1,4 @@
import {
checkServiceAccess,
cleanPatchRepos,
createPatch,
deletePatch,
@@ -14,6 +13,7 @@ import {
readPatchRepoFile,
updatePatch,
} from "@dokploy/server";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import {
@@ -21,6 +21,7 @@ import {
createTRPCRouter,
protectedProcedure,
} from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiCreatePatch,
apiDeletePatch,
@@ -29,47 +30,56 @@ import {
apiUpdatePatch,
} from "@/server/db/schema";
/**
* Resolves the serviceId from a patch record (applicationId or composeId).
* Throws if neither is set.
*/
const resolvePatchServiceId = (patch: {
applicationId: string | null;
composeId: string | null;
}): string => {
const serviceId = patch.applicationId ?? patch.composeId;
if (!serviceId) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Patch has no associated service",
});
}
return serviceId;
};
export const patchRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreatePatch)
.mutation(async ({ input, ctx }) => {
if (input.applicationId) {
const app = await findApplicationById(input.applicationId);
if (
app.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.applicationId,
ctx.session.activeOrganizationId,
"access",
);
}
} else if (input.composeId) {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
const serviceId = input.applicationId ?? input.composeId;
if (!serviceId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Either applicationId or composeId must be provided",
});
}
return await createPatch(input);
await checkServicePermissionAndAccess(ctx, serviceId, {
service: ["create"],
});
const result = await createPatch(input);
await audit(ctx, {
action: "create",
resourceType: "settings",
resourceId: result.patchId,
resourceName: result.filePath,
metadata: { type: "patch" },
});
return result;
}),
one: protectedProcedure.input(apiFindPatch).query(async ({ input }) => {
return await findPatchById(input.patchId);
one: protectedProcedure.input(apiFindPatch).query(async ({ input, ctx }) => {
const patch = await findPatchById(input.patchId);
const serviceId = resolvePatchServiceId(patch);
await checkServicePermissionAndAccess(ctx, serviceId, {
service: ["read"],
});
return patch;
}),
byEntityId: protectedProcedure
@@ -77,51 +87,70 @@ export const patchRouter = createTRPCRouter({
z.object({ id: z.string(), type: z.enum(["application", "compose"]) }),
)
.query(async ({ input, ctx }) => {
if (input.type === "application") {
const app = await findApplicationById(input.id);
if (
app.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
} else if (input.type === "compose") {
const compose = await findComposeById(input.id);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
}
const result = await findPatchesByEntityId(input.id, input.type);
return result;
await checkServicePermissionAndAccess(ctx, input.id, {
service: ["read"],
});
return await findPatchesByEntityId(input.id, input.type);
}),
update: protectedProcedure
.input(apiUpdatePatch)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const patch = await findPatchById(input.patchId);
const serviceId = resolvePatchServiceId(patch);
await checkServicePermissionAndAccess(ctx, serviceId, {
service: ["create"],
});
const { patchId, ...data } = input;
return await updatePatch(patchId, data);
const result = await updatePatch(patchId, data);
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceId: patchId,
resourceName: patch.filePath,
metadata: { type: "patch" },
});
return result;
}),
delete: protectedProcedure
.input(apiDeletePatch)
.mutation(async ({ input }) => {
return await deletePatch(input.patchId);
.mutation(async ({ input, ctx }) => {
const patch = await findPatchById(input.patchId);
const serviceId = resolvePatchServiceId(patch);
await checkServicePermissionAndAccess(ctx, serviceId, {
service: ["delete"],
});
const result = await deletePatch(input.patchId);
await audit(ctx, {
action: "delete",
resourceType: "settings",
resourceId: input.patchId,
resourceName: patch.filePath,
metadata: { type: "patch" },
});
return result;
}),
toggleEnabled: protectedProcedure
.input(apiTogglePatchEnabled)
.mutation(async ({ input }) => {
return await updatePatch(input.patchId, { enabled: input.enabled });
.mutation(async ({ input, ctx }) => {
const patch = await findPatchById(input.patchId);
const serviceId = resolvePatchServiceId(patch);
await checkServicePermissionAndAccess(ctx, serviceId, {
service: ["create"],
});
const result = await updatePatch(input.patchId, {
enabled: input.enabled,
});
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceId: input.patchId,
resourceName: patch.filePath,
metadata: { type: "patch", enabled: input.enabled },
});
return result;
}),
// Repository Operations
@@ -132,11 +161,21 @@ export const patchRouter = createTRPCRouter({
type: z.enum(["application", "compose"]),
}),
)
.mutation(async ({ input }) => {
return await ensurePatchRepo({
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.id, {
service: ["create"],
});
const result = await ensurePatchRepo({
type: input.type,
id: input.id,
});
await audit(ctx, {
action: "create",
resourceType: "settings",
resourceId: input.id,
metadata: { type: "ensurePatchRepo", serviceType: input.type },
});
return result;
}),
readRepoDirectories: protectedProcedure
@@ -148,36 +187,17 @@ export const patchRouter = createTRPCRouter({
}),
)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.id, {
service: ["read"],
});
let serverId: string | null = null;
if (input.type === "application") {
const app = await findApplicationById(input.id);
if (
app.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
serverId = app.serverId;
}
if (input.type === "compose") {
} else {
const compose = await findComposeById(input.id);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
serverId = compose.serverId;
}
return await readPatchRepoDirectory(input.repoPath, serverId);
}),
@@ -190,44 +210,22 @@ export const patchRouter = createTRPCRouter({
}),
)
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.id, {
service: ["read"],
});
let serverId: string | null = null;
if (input.type === "application") {
const app = await findApplicationById(input.id);
if (
app.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
serverId = app.serverId;
} else if (input.type === "compose") {
const compose = await findComposeById(input.id);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
serverId = compose.serverId;
} else {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Either applicationId or composeId must be provided",
});
const compose = await findComposeById(input.id);
serverId = compose.serverId;
}
const existingPatch = await findPatchByFilePath(
input.filePath,
input.id,
input.type,
);
// For delete patches, show current file content from repo (what will be deleted)
if (existingPatch?.type === "delete") {
try {
@@ -253,55 +251,43 @@ export const patchRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
if (input.type === "application") {
const app = await findApplicationById(input.id);
if (
app.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
} else if (input.type === "compose") {
const compose = await findComposeById(input.id);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
} else {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Either application or compose must be provided",
});
}
await checkServicePermissionAndAccess(ctx, input.id, {
service: ["create"],
});
const existingPatch = await findPatchByFilePath(
input.filePath,
input.id,
input.type,
);
if (!existingPatch) {
return await createPatch({
const result = await createPatch({
filePath: input.filePath,
content: input.content,
type: input.patchType,
applicationId: input.type === "application" ? input.id : undefined,
composeId: input.type === "compose" ? input.id : undefined,
});
await audit(ctx, {
action: "create",
resourceType: "settings",
resourceId: result.patchId,
resourceName: input.filePath,
metadata: { type: "saveFileAsPatch" },
});
return result;
}
return await updatePatch(existingPatch.patchId, {
const result = await updatePatch(existingPatch.patchId, {
content: input.content,
type: input.patchType,
});
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceId: existingPatch.patchId,
resourceName: input.filePath,
metadata: { type: "saveFileAsPatch" },
});
return result;
}),
markFileForDeletion: protectedProcedure
@@ -313,36 +299,34 @@ export const patchRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
if (input.type === "application") {
const app = await findApplicationById(input.id);
if (
app.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
} else if (input.type === "compose") {
const compose = await findComposeById(input.id);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this compose",
});
}
}
return await markPatchForDeletion(input.filePath, input.id, input.type);
await checkServicePermissionAndAccess(ctx, input.id, {
service: ["create"],
});
const result = await markPatchForDeletion(
input.filePath,
input.id,
input.type,
);
await audit(ctx, {
action: "delete",
resourceType: "settings",
resourceId: input.id,
resourceName: input.filePath,
metadata: { type: "markFileForDeletion" },
});
return result;
}),
cleanPatchRepos: adminProcedure
.input(z.object({ serverId: z.string().optional() }))
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
await cleanPatchRepos(input.serverId);
await audit(ctx, {
action: "delete",
resourceType: "settings",
resourceId: input.serverId || "local",
metadata: { type: "cleanPatchRepos" },
});
return true;
}),
});

View File

@@ -4,8 +4,10 @@ import {
removePortById,
updatePortById,
} from "@dokploy/server";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiCreatePort,
apiFindOnePort,
@@ -15,10 +17,19 @@ import {
export const portRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreatePort)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
await createPort(input);
return true;
await checkServicePermissionAndAccess(ctx, input.applicationId, {
service: ["create"],
});
const port = await createPort(input);
await audit(ctx, {
action: "create",
resourceType: "port",
resourceId: port.portId,
resourceName: `${port.publishedPort}:${port.targetPort}`,
});
return port;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -32,15 +43,11 @@ export const portRouter = createTRPCRouter({
.query(async ({ input, ctx }) => {
try {
const port = await finPortById(input.portId);
if (
port.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this port",
});
}
await checkServicePermissionAndAccess(
ctx,
port.application.applicationId,
{ service: ["read"] },
);
return port;
} catch (error) {
throw new TRPCError({
@@ -54,17 +61,20 @@ export const portRouter = createTRPCRouter({
.input(apiFindOnePort)
.mutation(async ({ input, ctx }) => {
const port = await finPortById(input.portId);
if (
port.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this port",
});
}
await checkServicePermissionAndAccess(
ctx,
port.application.applicationId,
{ service: ["delete"] },
);
try {
return await removePortById(input.portId);
const result = await removePortById(input.portId);
await audit(ctx, {
action: "delete",
resourceType: "port",
resourceId: port.portId,
resourceName: `${port.publishedPort}:${port.targetPort}`,
});
return result;
} catch (error) {
const message =
error instanceof Error ? error.message : "Error input: Deleting port";
@@ -78,17 +88,20 @@ export const portRouter = createTRPCRouter({
.input(apiUpdatePort)
.mutation(async ({ input, ctx }) => {
const port = await finPortById(input.portId);
if (
port.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this port",
});
}
await checkServicePermissionAndAccess(
ctx,
port.application.applicationId,
{ service: ["create"] },
);
try {
return await updatePortById(input.portId, input);
const result = await updatePortById(input.portId, input);
await audit(ctx, {
action: "update",
resourceType: "port",
resourceId: port.portId,
resourceName: `${port.publishedPort}:${port.targetPort}`,
});
return result;
} catch (error) {
const message =
error instanceof Error ? error.message : "Error updating the port";

View File

@@ -1,13 +1,10 @@
import {
addNewService,
checkPortInUse,
checkServiceAccess,
createMount,
createPostgres,
deployPostgres,
findBackupsByDbId,
findEnvironmentById,
findMemberById,
findPostgresById,
findProjectById,
getMountPath,
@@ -21,10 +18,17 @@ import {
stopServiceRemote,
updatePostgresById,
} from "@dokploy/server";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangePostgresStatus,
@@ -46,18 +50,10 @@ export const postgresRouter = createTRPCRouter({
.input(apiCreatePostgres)
.mutation(async ({ input, ctx }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
}
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
@@ -75,13 +71,7 @@ export const postgresRouter = createTRPCRouter({
const newPostgres = await createPostgres({
...input,
});
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
newPostgres.postgresId,
project.organizationId,
);
}
await addNewService(ctx, newPostgres.postgresId);
const mountPath = getMountPath(input.dockerImage);
@@ -93,6 +83,12 @@ export const postgresRouter = createTRPCRouter({
type: "volume",
});
await audit(ctx, {
action: "create",
resourceType: "service",
resourceId: newPostgres.postgresId,
resourceName: newPostgres.appName,
});
return newPostgres;
} catch (error) {
if (error instanceof TRPCError) {
@@ -108,14 +104,7 @@ export const postgresRouter = createTRPCRouter({
one: protectedProcedure
.input(apiFindOnePostgres)
.query(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.postgresId,
ctx.session.activeOrganizationId,
"access",
);
}
await checkServiceAccess(ctx, input.postgresId, "read");
const postgres = await findPostgresById(input.postgresId);
if (
@@ -133,18 +122,11 @@ export const postgresRouter = createTRPCRouter({
start: protectedProcedure
.input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
deployment: ["create"],
});
const service = await findPostgresById(input.postgresId);
if (
service.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this Postgres",
});
}
if (service.serverId) {
await startServiceRemote(service.serverId, service.appName);
} else {
@@ -154,21 +136,21 @@ export const postgresRouter = createTRPCRouter({
applicationStatus: "done",
});
await audit(ctx, {
action: "start",
resourceType: "service",
resourceId: service.postgresId,
resourceName: service.appName,
});
return service;
}),
stop: protectedProcedure
.input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
deployment: ["create"],
});
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this Postgres",
});
}
if (postgres.serverId) {
await stopServiceRemote(postgres.serverId, postgres.appName);
} else {
@@ -178,23 +160,22 @@ export const postgresRouter = createTRPCRouter({
applicationStatus: "idle",
});
await audit(ctx, {
action: "stop",
resourceType: "service",
resourceId: postgres.postgresId,
resourceName: postgres.appName,
});
return postgres;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortPostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
service: ["create"],
});
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
});
}
if (input.externalPort) {
const portCheck = await checkPortInUse(
input.externalPort,
@@ -212,21 +193,27 @@ export const postgresRouter = createTRPCRouter({
externalPort: input.externalPort,
});
await deployPostgres(input.postgresId);
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: postgres.postgresId,
resourceName: postgres.appName,
});
return postgres;
}),
deploy: protectedProcedure
.input(apiDeployPostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
deployment: ["create"],
});
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Postgres",
});
}
await audit(ctx, {
action: "deploy",
resourceType: "service",
resourceId: postgres.postgresId,
resourceName: postgres.appName,
});
return deployPostgres(input.postgresId);
}),
@@ -241,17 +228,9 @@ export const postgresRouter = createTRPCRouter({
})
.input(apiDeployPostgres)
.subscription(async function* ({ input, ctx, signal }) {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Postgres",
});
}
await checkServicePermissionAndAccess(ctx, input.postgresId, {
deployment: ["create"],
});
const queue: string[] = [];
const done = false;
@@ -276,32 +255,25 @@ export const postgresRouter = createTRPCRouter({
changeStatus: protectedProcedure
.input(apiChangePostgresStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
deployment: ["create"],
});
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this Postgres status",
});
}
await updatePostgresById(input.postgresId, {
applicationStatus: input.applicationStatus,
});
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: postgres.postgresId,
resourceName: postgres.appName,
});
return postgres;
}),
remove: protectedProcedure
.input(apiFindOnePostgres)
.mutation(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.postgresId,
ctx.session.activeOrganizationId,
"delete",
);
}
await checkServiceAccess(ctx, input.postgresId, "delete");
const postgres = await findPostgresById(input.postgresId);
if (
@@ -314,6 +286,12 @@ export const postgresRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "delete",
resourceType: "service",
resourceId: postgres.postgresId,
resourceName: postgres.appName,
});
const backups = await findBackupsByDbId(input.postgresId, "postgres");
const cleanupOperations = [
@@ -333,16 +311,9 @@ export const postgresRouter = createTRPCRouter({
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariablesPostgres)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
});
}
await checkServicePermissionAndAccess(ctx, input.postgresId, {
envVars: ["write"],
});
const service = await updatePostgresById(input.postgresId, {
env: input.env,
});
@@ -354,21 +325,20 @@ export const postgresRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: input.postgresId,
});
return true;
}),
reload: protectedProcedure
.input(apiResetPostgres)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.postgresId, {
deployment: ["create"],
});
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this Postgres",
});
}
if (postgres.serverId) {
await stopServiceRemote(postgres.serverId, postgres.appName);
} else {
@@ -386,22 +356,21 @@ export const postgresRouter = createTRPCRouter({
await updatePostgresById(input.postgresId, {
applicationStatus: "done",
});
await audit(ctx, {
action: "reload",
resourceType: "service",
resourceId: postgres.postgresId,
resourceName: postgres.appName,
});
return true;
}),
update: protectedProcedure
.input(apiUpdatePostgres)
.mutation(async ({ input, ctx }) => {
const { postgresId, ...rest } = input;
const postgres = await findPostgresById(postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this Postgres",
});
}
await checkServicePermissionAndAccess(ctx, postgresId, {
service: ["create"],
});
const service = await updatePostgresById(postgresId, {
...rest,
@@ -414,6 +383,12 @@ export const postgresRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: postgresId,
resourceName: service.appName,
});
return true;
}),
move: protectedProcedure
@@ -424,31 +399,10 @@ export const postgresRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this postgres",
});
}
await checkServicePermissionAndAccess(ctx, input.postgresId, {
service: ["create"],
});
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
// Update the postgres's projectId
const updatedPostgres = await db
.update(postgresTable)
.set({
@@ -465,24 +419,28 @@ export const postgresRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "move",
resourceType: "service",
resourceId: updatedPostgres.postgresId,
resourceName: updatedPostgres.appName,
});
return updatedPostgres;
}),
rebuild: protectedProcedure
.input(apiRebuildPostgres)
.mutation(async ({ input, ctx }) => {
const postgres = await findPostgresById(input.postgresId);
if (
postgres.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this Postgres database",
});
}
await checkServicePermissionAndAccess(ctx, input.postgresId, {
deployment: ["create"],
});
await rebuildDatabase(postgres.postgresId, "postgres");
await rebuildDatabase(input.postgresId, "postgres");
await audit(ctx, {
action: "rebuild",
resourceType: "service",
resourceId: input.postgresId,
});
return true;
}),
search: protectedProcedure
@@ -538,19 +496,18 @@ export const postgresRouter = createTRPCRouter({
),
);
}
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${postgresTable.postgresId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const { accessedServices } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${postgresTable.postgresId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db

View File

@@ -5,8 +5,9 @@ import {
IS_CLOUD,
removePreviewDeployment,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { apiFindAllByApplication } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup";
@@ -17,53 +18,46 @@ export const previewDeploymentRouter = createTRPCRouter({
all: protectedProcedure
.input(apiFindAllByApplication)
.query(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
await checkServicePermissionAndAccess(ctx, input.applicationId, {
deployment: ["read"],
});
return await findPreviewDeploymentsByApplicationId(input.applicationId);
}),
delete: protectedProcedure
.input(z.object({ previewDeploymentId: z.string() }))
.mutation(async ({ input, ctx }) => {
const previewDeployment = await findPreviewDeploymentById(
input.previewDeploymentId,
);
if (
previewDeployment.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this preview deployment",
});
}
await removePreviewDeployment(input.previewDeploymentId);
return true;
}),
one: protectedProcedure
.input(z.object({ previewDeploymentId: z.string() }))
.query(async ({ input, ctx }) => {
const previewDeployment = await findPreviewDeploymentById(
input.previewDeploymentId,
);
if (
previewDeployment.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this preview deployment",
});
}
await checkServicePermissionAndAccess(
ctx,
previewDeployment.applicationId,
{ deployment: ["read"] },
);
return previewDeployment;
}),
delete: protectedProcedure
.input(z.object({ previewDeploymentId: z.string() }))
.mutation(async ({ input, ctx }) => {
const previewDeployment = await findPreviewDeploymentById(
input.previewDeploymentId,
);
await checkServicePermissionAndAccess(
ctx,
previewDeployment.applicationId,
{ deployment: ["cancel"] },
);
await removePreviewDeployment(input.previewDeploymentId);
await audit(ctx, {
action: "delete",
resourceType: "previewDeployment",
resourceId: input.previewDeploymentId,
});
return true;
}),
redeploy: protectedProcedure
.input(
z.object({
@@ -76,15 +70,11 @@ export const previewDeploymentRouter = createTRPCRouter({
const previewDeployment = await findPreviewDeploymentById(
input.previewDeploymentId,
);
if (
previewDeployment.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to redeploy this preview deployment",
});
}
await checkServicePermissionAndAccess(
ctx,
previewDeployment.applicationId,
{ deployment: ["create"] },
);
const application = await findApplicationById(
previewDeployment.applicationId,
);
@@ -103,6 +93,11 @@ export const previewDeploymentRouter = createTRPCRouter({
deploy(jobData).catch((error) => {
console.error("Background deployment failed:", error);
});
await audit(ctx, {
action: "redeploy",
resourceType: "previewDeployment",
resourceId: input.previewDeploymentId,
});
return true;
}
await myQueue.add(
@@ -113,6 +108,11 @@ export const previewDeploymentRouter = createTRPCRouter({
removeOnFail: true,
},
);
await audit(ctx, {
action: "redeploy",
resourceType: "previewDeployment",
resourceId: input.previewDeploymentId,
});
return true;
}),
});

View File

@@ -1,7 +1,4 @@
import {
addNewEnvironment,
addNewProject,
checkProjectAccess,
createApplication,
createBackup,
createCompose,
@@ -22,7 +19,6 @@ import {
findComposeById,
findEnvironmentById,
findMariadbById,
findMemberById,
findMongoById,
findMySqlById,
findPostgresById,
@@ -32,15 +28,23 @@ import {
IS_CLOUD,
updateProjectById,
} from "@dokploy/server";
import {
addNewEnvironment,
addNewProject,
checkPermission,
checkProjectAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import type { AnyPgColumn } from "drizzle-orm/pg-core";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import {
apiCreateProject,
@@ -63,13 +67,7 @@ export const projectRouter = createTRPCRouter({
.input(apiCreateProject)
.mutation(async ({ ctx, input }) => {
try {
if (ctx.user.role === "member") {
await checkProjectAccess(
ctx.user.id,
"create",
ctx.session.activeOrganizationId,
);
}
await checkProjectAccess(ctx, "create");
const admin = await findUserById(ctx.user.ownerId);
@@ -84,20 +82,16 @@ export const projectRouter = createTRPCRouter({
input,
ctx.session.activeOrganizationId,
);
if (ctx.user.role === "member") {
await addNewProject(
ctx.user.id,
project.project.projectId,
ctx.session.activeOrganizationId,
);
await addNewProject(ctx, project.project.projectId);
await addNewEnvironment(
ctx.user.id,
project?.environment?.environmentId || "",
ctx.session.activeOrganizationId,
);
}
await addNewEnvironment(ctx, project?.environment?.environmentId || "");
await audit(ctx, {
action: "create",
resourceType: "project",
resourceId: project.project.projectId,
resourceName: project.project.name,
});
return project;
} catch (error) {
throw new TRPCError({
@@ -111,18 +105,18 @@ export const projectRouter = createTRPCRouter({
one: protectedProcedure
.input(apiFindOneProject)
.query(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedServices, accessedProjects } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
await checkProjectAccess(
ctx.user.id,
"access",
ctx.session.activeOrganizationId,
input.projectId,
);
if (!accessedProjects.includes(input.projectId)) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}
const project = await db.query.projects.findFirst({
where: and(
@@ -189,15 +183,14 @@ export const projectRouter = createTRPCRouter({
return project;
}),
all: protectedProcedure.query(async ({ ctx }) => {
if (ctx.user.role === "member") {
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedProjects, accessedEnvironments, accessedServices } =
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
await findMemberByUserId(ctx.user.id, ctx.session.activeOrganizationId);
if (accessedProjects.length === 0) {
return [];
}
// Build environment filter
const environmentFilter =
accessedEnvironments.length === 0
? sql`false`
@@ -348,105 +341,106 @@ export const projectRouter = createTRPCRouter({
});
}),
/** All projects with full environments and services for the admin permissions UI. Admin only. */
allForPermissions: adminProcedure.query(async ({ ctx }) => {
return await db.query.projects.findMany({
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
orderBy: desc(projects.createdAt),
columns: {
projectId: true,
name: true,
},
with: {
environments: {
columns: {
environmentId: true,
name: true,
isDefault: true,
},
with: {
applications: {
columns: {
applicationId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
allForPermissions: withPermission("member", "update").query(
async ({ ctx }) => {
return await db.query.projects.findMany({
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
orderBy: desc(projects.createdAt),
columns: {
projectId: true,
name: true,
},
with: {
environments: {
columns: {
environmentId: true,
name: true,
isDefault: true,
},
mariadb: {
columns: {
mariadbId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
with: {
applications: {
columns: {
applicationId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
},
postgres: {
columns: {
postgresId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
mariadb: {
columns: {
mariadbId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
},
mysql: {
columns: {
mysqlId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
postgres: {
columns: {
postgresId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
},
mongo: {
columns: {
mongoId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
mysql: {
columns: {
mysqlId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
},
redis: {
columns: {
redisId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
mongo: {
columns: {
mongoId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
},
compose: {
columns: {
composeId: true,
appName: true,
name: true,
createdAt: true,
composeStatus: true,
description: true,
serverId: true,
redis: {
columns: {
redisId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
compose: {
columns: {
composeId: true,
appName: true,
name: true,
createdAt: true,
composeStatus: true,
description: true,
serverId: true,
},
},
},
},
},
},
});
}),
});
},
),
search: protectedProcedure
.input(
@@ -482,8 +476,8 @@ export const projectRouter = createTRPCRouter({
);
}
if (ctx.user.role === "member") {
const { accessedProjects } = await findMemberById(
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedProjects } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
@@ -529,13 +523,6 @@ export const projectRouter = createTRPCRouter({
.input(apiRemoveProject)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.role === "member") {
await checkProjectAccess(
ctx.user.id,
"delete",
ctx.session.activeOrganizationId,
);
}
const currentProject = await findProjectById(input.projectId);
if (
currentProject.organizationId !== ctx.session.activeOrganizationId
@@ -545,8 +532,15 @@ export const projectRouter = createTRPCRouter({
message: "You are not authorized to delete this project",
});
}
await checkProjectAccess(ctx, "delete", input.projectId);
const deletedProject = await deleteProject(input.projectId);
await audit(ctx, {
action: "delete",
resourceType: "project",
resourceId: currentProject.projectId,
resourceName: currentProject.name,
});
return deletedProject;
} catch (error) {
throw error;
@@ -565,10 +559,36 @@ export const projectRouter = createTRPCRouter({
message: "You are not authorized to update this project",
});
}
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedProjects } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (!accessedProjects.includes(input.projectId)) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}
}
if (input.env !== undefined) {
await checkPermission(ctx, { projectEnvVars: ["write"] });
}
const project = await updateProjectById(input.projectId, {
...input,
});
if (project) {
await audit(ctx, {
action: "update",
resourceType: "project",
resourceId: input.projectId,
resourceName: project.name,
});
}
return project;
} catch (error) {
throw error;
@@ -602,15 +622,8 @@ export const projectRouter = createTRPCRouter({
)
.mutation(async ({ ctx, input }) => {
try {
if (ctx.user.role === "member") {
await checkProjectAccess(
ctx.user.id,
"create",
ctx.session.activeOrganizationId,
);
}
await checkProjectAccess(ctx, "create");
// Get source project
const sourceEnvironment = input.duplicateInSameProject
? await findEnvironmentById(input.sourceEnvironmentId)
: null;
@@ -626,7 +639,24 @@ export const projectRouter = createTRPCRouter({
});
}
// Create new project or use existing one
if (
input.duplicateInSameProject &&
sourceEnvironment &&
ctx.user.role !== "owner" &&
ctx.user.role !== "admin"
) {
const { accessedProjects } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (!accessedProjects.includes(sourceEnvironment.project.projectId)) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}
}
const targetProject = input.duplicateInSameProject
? sourceEnvironment
: await createProject(
@@ -643,7 +673,6 @@ export const projectRouter = createTRPCRouter({
if (input.includeServices) {
const servicesToDuplicate = input.selectedServices || [];
// Helper function to duplicate a service
const duplicateService = async (id: string, type: string) => {
switch (type) {
case "application": {
@@ -947,20 +976,22 @@ export const projectRouter = createTRPCRouter({
}
};
// Duplicate selected services
for (const service of servicesToDuplicate) {
await duplicateService(service.id, service.type);
}
}
if (!input.duplicateInSameProject && ctx.user.role === "member") {
await addNewProject(
ctx.user.id,
targetProject?.projectId || "",
ctx.session.activeOrganizationId,
);
if (!input.duplicateInSameProject) {
await addNewProject(ctx, targetProject?.projectId || "");
}
await audit(ctx, {
action: "create",
resourceType: "project",
resourceId: targetProject?.projectId || "",
resourceName: input.name,
metadata: { duplicatedFrom: input.sourceEnvironmentId },
});
return targetProject;
} catch (error) {
throw new TRPCError({

View File

@@ -0,0 +1,67 @@
import { getAuditLogs } from "@dokploy/server/services/proprietary/audit-log";
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, withPermission } from "../../trpc";
export const auditLogRouter = createTRPCRouter({
all: withPermission("auditLog", "read")
.use(async ({ ctx, next }) => {
const licensed = await hasValidLicense(ctx.session.activeOrganizationId);
if (!licensed) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Valid enterprise license required",
});
}
return next();
})
.input(
z.object({
userId: z.string().optional(),
userEmail: z.string().optional(),
resourceName: z.string().optional(),
action: z
.enum([
"create",
"update",
"delete",
"deploy",
"cancel",
"redeploy",
"login",
"logout",
])
.optional(),
resourceType: z
.enum([
"project",
"service",
"environment",
"deployment",
"user",
"customRole",
"domain",
"certificate",
"registry",
"server",
"sshKey",
"gitProvider",
"notification",
"settings",
"session",
])
.optional(),
from: z.date().optional(),
to: z.date().optional(),
limit: z.number().min(1).max(500).default(50),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
return getAuditLogs({
organizationId: ctx.session.activeOrganizationId,
...input,
});
}),
});

View File

@@ -0,0 +1,321 @@
import { db } from "@dokploy/server/db";
import { member, organizationRole, user } from "@dokploy/server/db/schema";
import { statements } from "@dokploy/server/lib/access-control";
import { TRPCError } from "@trpc/server";
import { and, count, eq } from "drizzle-orm";
import { z } from "zod";
import {
createTRPCRouter,
enterpriseProcedure,
protectedProcedure,
} from "../../trpc";
import { audit } from "../../utils/audit";
const permissionsSchema = z.record(z.string(), z.array(z.string()));
export const customRoleRouter = createTRPCRouter({
all: protectedProcedure.query(async ({ ctx }) => {
const [roles, memberCounts] = await Promise.all([
db.query.organizationRole.findMany({
where: eq(
organizationRole.organizationId,
ctx.session.activeOrganizationId,
),
}),
db
.select({ role: member.role, count: count() })
.from(member)
.where(eq(member.organizationId, ctx.session.activeOrganizationId))
.groupBy(member.role),
]);
const memberCountByRole = new Map(
memberCounts.map((r) => [r.role, r.count]),
);
const roleMap = new Map<
string,
{
role: string;
permissions: Record<string, string[]>;
createdAt: Date;
ids: string[];
memberCount: number;
}
>();
for (const entry of roles) {
const existing = roleMap.get(entry.role);
const parsed = JSON.parse(entry.permission) as Record<string, string[]>;
if (existing) {
for (const [resource, actions] of Object.entries(parsed)) {
existing.permissions[resource] = [
...new Set([...(existing.permissions[resource] ?? []), ...actions]),
];
}
existing.ids.push(entry.id);
} else {
roleMap.set(entry.role, {
role: entry.role,
permissions: parsed,
createdAt: entry.createdAt,
ids: [entry.id],
memberCount: memberCountByRole.get(entry.role) ?? 0,
});
}
}
return Array.from(roleMap.values());
}),
create: enterpriseProcedure
.input(
z.object({
roleName: z
.string()
.min(1)
.max(50)
.refine(
(name) => !["owner", "admin", "member"].includes(name),
"Cannot use reserved role names (owner, admin, member)",
),
permissions: permissionsSchema,
}),
)
.mutation(async ({ input, ctx }) => {
const existingRoles = await db.query.organizationRole.findMany({
where: eq(
organizationRole.organizationId,
ctx.session.activeOrganizationId,
),
});
const uniqueRoleNames = new Set(existingRoles.map((r) => r.role));
if (uniqueRoleNames.size >= 10) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Maximum of 10 custom roles per organization reached",
});
}
if (uniqueRoleNames.has(input.roleName)) {
throw new TRPCError({
code: "CONFLICT",
message: `Role "${input.roleName}" already exists`,
});
}
validatePermissions(input.permissions);
const [created] = await db
.insert(organizationRole)
.values({
organizationId: ctx.session.activeOrganizationId,
role: input.roleName,
permission: JSON.stringify(input.permissions),
})
.returning();
await audit(ctx, {
action: "create",
resourceType: "customRole",
resourceName: input.roleName,
});
return created;
}),
update: enterpriseProcedure
.input(
z.object({
roleName: z.string().min(1),
newRoleName: z
.string()
.min(1)
.max(50)
.refine(
(name) => !["owner", "admin", "member"].includes(name),
"Cannot use reserved role names (owner, admin, member)",
)
.optional(),
permissions: permissionsSchema,
}),
)
.mutation(async ({ input, ctx }) => {
if (["owner", "admin", "member"].includes(input.roleName)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Cannot modify built-in roles",
});
}
const effectiveRoleName = input.newRoleName ?? input.roleName;
if (input.newRoleName && input.newRoleName !== input.roleName) {
const existing = await db.query.organizationRole.findFirst({
where: and(
eq(
organizationRole.organizationId,
ctx.session.activeOrganizationId,
),
eq(organizationRole.role, input.newRoleName),
),
});
if (existing) {
throw new TRPCError({
code: "CONFLICT",
message: `Role "${input.newRoleName}" already exists`,
});
}
await db
.update(member)
.set({ role: input.newRoleName })
.where(
and(
eq(member.organizationId, ctx.session.activeOrganizationId),
eq(member.role, input.roleName),
),
);
}
validatePermissions(input.permissions);
const [updated] = await db
.update(organizationRole)
.set({
role: effectiveRoleName,
permission: JSON.stringify(input.permissions),
})
.where(
and(
eq(
organizationRole.organizationId,
ctx.session.activeOrganizationId,
),
eq(organizationRole.role, input.roleName),
),
)
.returning();
await audit(ctx, {
action: "update",
resourceType: "customRole",
resourceName: effectiveRoleName,
});
return updated;
}),
remove: enterpriseProcedure
.input(
z.object({
roleName: z.string().min(1),
}),
)
.mutation(async ({ input, ctx }) => {
if (["owner", "admin", "member"].includes(input.roleName)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Cannot delete built-in roles",
});
}
const assignedMembers = await db.query.member.findMany({
where: and(
eq(member.organizationId, ctx.session.activeOrganizationId),
eq(member.role, input.roleName),
),
});
if (assignedMembers.length > 0) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Cannot delete role "${input.roleName}": ${assignedMembers.length} member(s) are currently assigned to it. Reassign them first.`,
});
}
const deleted = await db
.delete(organizationRole)
.where(
and(
eq(
organizationRole.organizationId,
ctx.session.activeOrganizationId,
),
eq(organizationRole.role, input.roleName),
),
)
.returning();
if (deleted.length === 0) {
throw new TRPCError({
code: "NOT_FOUND",
message: `Role "${input.roleName}" not found`,
});
}
await audit(ctx, {
action: "delete",
resourceType: "customRole",
resourceName: input.roleName,
});
return { deleted: deleted.length };
}),
membersByRole: protectedProcedure
.input(z.object({ roleName: z.string().min(1) }))
.query(async ({ input, ctx }) => {
const members = await db
.select({
id: member.id,
userId: member.userId,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
})
.from(member)
.innerJoin(user, eq(member.userId, user.id))
.where(
and(
eq(member.organizationId, ctx.session.activeOrganizationId),
eq(member.role, input.roleName),
),
);
return members;
}),
getStatements: protectedProcedure.query(() => {
return statements;
}),
});
const INTERNAL_RESOURCES = ["organization", "invitation", "team", "ac"];
function validatePermissions(permissions: Record<string, string[]>) {
for (const [resource, actions] of Object.entries(permissions)) {
if (INTERNAL_RESOURCES.includes(resource)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Resource "${resource}" is managed internally and cannot be assigned to custom roles`,
});
}
if (!(resource in statements)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Unknown resource: ${resource}`,
});
}
const validActions = statements[resource as keyof typeof statements];
for (const action of actions) {
if (!validActions.includes(action as never)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Invalid action "${action}" for resource "${resource}". Valid actions: ${validActions.join(", ")}`,
});
}
}
}
}

View File

@@ -4,7 +4,11 @@ import { hasValidLicense, validateLicenseKey } from "@dokploy/server/index";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { adminProcedure, createTRPCRouter } from "@/server/api/trpc";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
} from "@/server/api/trpc";
import {
activateLicenseKey,
deactivateLicenseKey,
@@ -183,7 +187,7 @@ export const licenseKeyRouter = createTRPCRouter({
licenseKey: currentUser.licenseKey ?? "",
};
}),
haveValidLicenseKey: adminProcedure.query(async ({ ctx }) => {
haveValidLicenseKey: protectedProcedure.query(async ({ ctx }) => {
return await hasValidLicense(ctx.session.activeOrganizationId);
}),
updateEnterpriseSettings: adminProcedure

View File

@@ -0,0 +1,106 @@
import {
getWebServerSettings,
IS_CLOUD,
updateWebServerSettings,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { apiUpdateWhitelabeling } from "@/server/db/schema";
import {
createTRPCRouter,
enterpriseProcedure,
protectedProcedure,
publicProcedure,
} from "../../trpc";
export const whitelabelingRouter = createTRPCRouter({
get: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return null;
}
const settings = await getWebServerSettings();
return settings?.whitelabelingConfig ?? null;
}),
update: enterpriseProcedure
.input(apiUpdateWhitelabeling)
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Whitelabeling is not available in Cloud",
});
}
if (ctx.user.role !== "owner") {
throw new TRPCError({
code: "FORBIDDEN",
message: "Only the owner can update whitelabeling settings",
});
}
await updateWebServerSettings({
whitelabelingConfig: input.whitelabelingConfig,
});
return { success: true };
}),
reset: enterpriseProcedure.mutation(async ({ ctx }) => {
if (IS_CLOUD) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Whitelabeling is not available in Cloud",
});
}
if (ctx.user.role !== "owner") {
throw new TRPCError({
code: "FORBIDDEN",
message: "Only the owner can reset whitelabeling settings",
});
}
await updateWebServerSettings({
whitelabelingConfig: {
appName: null,
appDescription: null,
logoUrl: null,
faviconUrl: null,
customCss: null,
loginLogoUrl: null,
supportUrl: null,
docsUrl: null,
errorPageTitle: null,
errorPageDescription: null,
metaTitle: null,
footerText: null,
},
});
return { success: true };
}),
// Public endpoint only for unauthenticated pages (login, register, error)
// Returns only the fields needed for public pages
getPublic: publicProcedure.query(async () => {
if (IS_CLOUD) {
return null;
}
const settings = await getWebServerSettings();
const config = settings?.whitelabelingConfig;
if (!config) return null;
return {
appName: config.appName,
appDescription: config.appDescription,
logoUrl: config.logoUrl,
loginLogoUrl: config.loginLogoUrl,
faviconUrl: config.faviconUrl,
customCss: config.customCss,
metaTitle: config.metaTitle,
errorPageTitle: config.errorPageTitle,
errorPageDescription: config.errorPageDescription,
footerText: config.footerText,
};
}),
});

View File

@@ -1,80 +1,74 @@
import {
createRedirect,
findApplicationById,
findRedirectById,
removeRedirectById,
updateRedirectById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateRedirect,
apiFindOneRedirect,
apiUpdateRedirect,
} from "@/server/db/schema";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const redirectsRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateRedirect)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return await createRedirect(input);
await checkServicePermissionAndAccess(ctx, input.applicationId, {
service: ["create"],
});
await createRedirect(input);
await audit(ctx, {
action: "create",
resourceType: "redirect",
resourceId: input.applicationId,
resourceName: input.regex,
});
return true;
}),
one: protectedProcedure
.input(apiFindOneRedirect)
.query(async ({ input, ctx }) => {
const redirect = await findRedirectById(input.redirectId);
const application = await findApplicationById(redirect.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return findRedirectById(input.redirectId);
await checkServicePermissionAndAccess(ctx, redirect.applicationId, {
service: ["read"],
});
return redirect;
}),
delete: protectedProcedure
.input(apiFindOneRedirect)
.mutation(async ({ input, ctx }) => {
const redirect = await findRedirectById(input.redirectId);
const application = await findApplicationById(redirect.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return removeRedirectById(input.redirectId);
await checkServicePermissionAndAccess(ctx, redirect.applicationId, {
service: ["delete"],
});
const result = await removeRedirectById(input.redirectId);
await audit(ctx, {
action: "delete",
resourceType: "redirect",
resourceId: input.redirectId,
});
return result;
}),
update: protectedProcedure
.input(apiUpdateRedirect)
.mutation(async ({ input, ctx }) => {
const redirect = await findRedirectById(input.redirectId);
const application = await findApplicationById(redirect.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return updateRedirectById(input.redirectId, input);
await checkServicePermissionAndAccess(ctx, redirect.applicationId, {
service: ["create"],
});
const result = await updateRedirectById(input.redirectId, input);
await audit(ctx, {
action: "update",
resourceType: "redirect",
resourceId: input.redirectId,
});
return result;
}),
});

View File

@@ -1,12 +1,9 @@
import {
addNewService,
checkPortInUse,
checkServiceAccess,
createMount,
createRedis,
deployRedis,
findEnvironmentById,
findMemberById,
findProjectById,
findRedisById,
IS_CLOUD,
@@ -19,10 +16,17 @@ import {
stopServiceRemote,
updateRedisById,
} from "@dokploy/server";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangeRedisStatus,
@@ -42,18 +46,10 @@ export const redisRouter = createTRPCRouter({
.input(apiCreateRedis)
.mutation(async ({ input, ctx }) => {
try {
// Get project from environment
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
project.projectId,
ctx.session.activeOrganizationId,
"create",
);
}
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
@@ -71,13 +67,7 @@ export const redisRouter = createTRPCRouter({
const newRedis = await createRedis({
...input,
});
if (ctx.user.role === "member") {
await addNewService(
ctx.user.id,
newRedis.redisId,
project.organizationId,
);
}
await addNewService(ctx, newRedis.redisId);
await createMount({
serviceId: newRedis.redisId,
@@ -87,6 +77,12 @@ export const redisRouter = createTRPCRouter({
type: "volume",
});
await audit(ctx, {
action: "create",
resourceType: "service",
resourceId: newRedis.redisId,
resourceName: newRedis.appName,
});
return newRedis;
} catch (error) {
throw error;
@@ -95,14 +91,7 @@ export const redisRouter = createTRPCRouter({
one: protectedProcedure
.input(apiFindOneRedis)
.query(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.redisId,
ctx.session.activeOrganizationId,
"access",
);
}
await checkServiceAccess(ctx, input.redisId, "read");
const redis = await findRedisById(input.redisId);
if (
@@ -120,16 +109,10 @@ export const redisRouter = createTRPCRouter({
start: protectedProcedure
.input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, {
deployment: ["create"],
});
const redis = await findRedisById(input.redisId);
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to start this Redis",
});
}
if (redis.serverId) {
await startServiceRemote(redis.serverId, redis.appName);
@@ -140,21 +123,21 @@ export const redisRouter = createTRPCRouter({
applicationStatus: "done",
});
await audit(ctx, {
action: "start",
resourceType: "service",
resourceId: redis.redisId,
resourceName: redis.appName,
});
return redis;
}),
reload: protectedProcedure
.input(apiResetRedis)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, {
deployment: ["create"],
});
const redis = await findRedisById(input.redisId);
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to reload this Redis",
});
}
if (redis.serverId) {
await stopServiceRemote(redis.serverId, redis.appName);
} else {
@@ -172,22 +155,22 @@ export const redisRouter = createTRPCRouter({
await updateRedisById(input.redisId, {
applicationStatus: "done",
});
await audit(ctx, {
action: "reload",
resourceType: "service",
resourceId: redis.redisId,
resourceName: redis.appName,
});
return true;
}),
stop: protectedProcedure
.input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, {
deployment: ["create"],
});
const redis = await findRedisById(input.redisId);
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to stop this Redis",
});
}
if (redis.serverId) {
await stopServiceRemote(redis.serverId, redis.appName);
} else {
@@ -197,21 +180,21 @@ export const redisRouter = createTRPCRouter({
applicationStatus: "idle",
});
await audit(ctx, {
action: "stop",
resourceType: "service",
resourceId: redis.redisId,
resourceName: redis.appName,
});
return redis;
}),
saveExternalPort: protectedProcedure
.input(apiSaveExternalPortRedis)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, {
service: ["create"],
});
const redis = await findRedisById(input.redisId);
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this external port",
});
}
if (input.externalPort) {
const portCheck = await checkPortInUse(
@@ -230,21 +213,27 @@ export const redisRouter = createTRPCRouter({
externalPort: input.externalPort,
});
await deployRedis(input.redisId);
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: redis.redisId,
resourceName: redis.appName,
});
return redis;
}),
deploy: protectedProcedure
.input(apiDeployRedis)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, {
deployment: ["create"],
});
const redis = await findRedisById(input.redisId);
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Redis",
});
}
await audit(ctx, {
action: "deploy",
resourceType: "service",
resourceId: redis.redisId,
resourceName: redis.appName,
});
return deployRedis(input.redisId);
}),
deployWithLogs: protectedProcedure
@@ -258,16 +247,9 @@ export const redisRouter = createTRPCRouter({
})
.input(apiDeployRedis)
.subscription(async function* ({ input, ctx, signal }) {
const redis = await findRedisById(input.redisId);
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to deploy this Redis",
});
}
await checkServicePermissionAndAccess(ctx, input.redisId, {
deployment: ["create"],
});
const queue: string[] = [];
const done = false;
@@ -290,32 +272,25 @@ export const redisRouter = createTRPCRouter({
changeStatus: protectedProcedure
.input(apiChangeRedisStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.redisId, {
deployment: ["create"],
});
const mongo = await findRedisById(input.redisId);
if (
mongo.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to change this Redis status",
});
}
await updateRedisById(input.redisId, {
applicationStatus: input.applicationStatus,
});
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: mongo.redisId,
resourceName: mongo.appName,
});
return mongo;
}),
remove: protectedProcedure
.input(apiFindOneRedis)
.mutation(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
await checkServiceAccess(
ctx.user.id,
input.redisId,
ctx.session.activeOrganizationId,
"delete",
);
}
await checkServiceAccess(ctx, input.redisId, "delete");
const redis = await findRedisById(input.redisId);
@@ -328,6 +303,12 @@ export const redisRouter = createTRPCRouter({
message: "You are not authorized to delete this Redis",
});
}
await audit(ctx, {
action: "delete",
resourceType: "service",
resourceId: redis.redisId,
resourceName: redis.appName,
});
const cleanupOperations = [
async () => await removeService(redis?.appName, redis.serverId),
async () => await removeRedisById(input.redisId),
@@ -344,16 +325,9 @@ export const redisRouter = createTRPCRouter({
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariablesRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to save this environment",
});
}
await checkServicePermissionAndAccess(ctx, input.redisId, {
envVars: ["write"],
});
const updatedRedis = await updateRedisById(input.redisId, {
env: input.env,
});
@@ -365,12 +339,20 @@ export const redisRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: input.redisId,
});
return true;
}),
update: protectedProcedure
.input(apiUpdateRedis)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const { redisId, ...rest } = input;
await checkServicePermissionAndAccess(ctx, redisId, {
service: ["create"],
});
const redis = await updateRedisById(redisId, {
...rest,
});
@@ -382,6 +364,12 @@ export const redisRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: redisId,
resourceName: redis.appName,
});
return true;
}),
move: protectedProcedure
@@ -392,31 +380,10 @@ export const redisRouter = createTRPCRouter({
}),
)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move this redis",
});
}
await checkServicePermissionAndAccess(ctx, input.redisId, {
service: ["create"],
});
const targetEnvironment = await findEnvironmentById(
input.targetEnvironmentId,
);
if (
targetEnvironment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to move to this environment",
});
}
// Update the redis's projectId
const updatedRedis = await db
.update(redisTable)
.set({
@@ -433,23 +400,27 @@ export const redisRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "move",
resourceType: "service",
resourceId: updatedRedis.redisId,
resourceName: updatedRedis.appName,
});
return updatedRedis;
}),
rebuild: protectedProcedure
.input(apiRebuildRedis)
.mutation(async ({ input, ctx }) => {
const redis = await findRedisById(input.redisId);
if (
redis.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rebuild this Redis database",
});
}
await checkServicePermissionAndAccess(ctx, input.redisId, {
deployment: ["create"],
});
await rebuildDatabase(redis.redisId, "redis");
await rebuildDatabase(input.redisId, "redis");
await audit(ctx, {
action: "rebuild",
resourceType: "service",
resourceId: input.redisId,
});
return true;
}),
search: protectedProcedure
@@ -498,19 +469,18 @@ export const redisRouter = createTRPCRouter({
ilike(redisTable.description ?? "", `%${input.description.trim()}%`),
);
}
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${redisTable.redisId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const { accessedServices } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedServices.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${redisTable.redisId} IN (${sql.join(
accessedServices.map((id) => sql`${id}`),
sql`, `,
)})`,
);
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db

View File

@@ -19,14 +19,22 @@ import {
apiUpdateRegistry,
registry,
} from "@/server/db/schema";
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, withPermission } from "../trpc";
export const registryRouter = createTRPCRouter({
create: adminProcedure
create: withPermission("registry", "create")
.input(apiCreateRegistry)
.mutation(async ({ ctx, input }) => {
return await createRegistry(input, ctx.session.activeOrganizationId);
const reg = await createRegistry(input, ctx.session.activeOrganizationId);
await audit(ctx, {
action: "create",
resourceType: "registry",
resourceId: reg.registryId,
resourceName: reg.registryName,
});
return reg;
}),
remove: adminProcedure
remove: withPermission("registry", "delete")
.input(apiRemoveRegistry)
.mutation(async ({ ctx, input }) => {
const registry = await findRegistryById(input.registryId);
@@ -36,9 +44,15 @@ export const registryRouter = createTRPCRouter({
message: "You are not allowed to delete this registry",
});
}
await audit(ctx, {
action: "delete",
resourceType: "registry",
resourceId: registry.registryId,
resourceName: registry.registryName,
});
return await removeRegistry(input.registryId);
}),
update: protectedProcedure
update: withPermission("registry", "create")
.input(apiUpdateRegistry)
.mutation(async ({ input, ctx }) => {
const { registryId, ...rest } = input;
@@ -60,15 +74,21 @@ export const registryRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "registry",
resourceId: registryId,
resourceName: registry.registryName,
});
return true;
}),
all: protectedProcedure.query(async ({ ctx }) => {
all: withPermission("registry", "read").query(async ({ ctx }) => {
const registryResponse = await db.query.registry.findMany({
where: eq(registry.organizationId, ctx.session.activeOrganizationId),
});
return registryResponse;
}),
one: adminProcedure
one: withPermission("registry", "read")
.input(apiFindOneRegistry)
.query(async ({ input, ctx }) => {
const registry = await findRegistryById(input.registryId);
@@ -80,7 +100,7 @@ export const registryRouter = createTRPCRouter({
}
return registry;
}),
testRegistry: protectedProcedure
testRegistry: withPermission("registry", "read")
.input(apiTestRegistry)
.mutation(async ({ input }) => {
try {
@@ -122,11 +142,10 @@ export const registryRouter = createTRPCRouter({
});
}
}),
testRegistryById: protectedProcedure
testRegistryById: withPermission("registry", "read")
.input(apiTestRegistryById)
.mutation(async ({ input, ctx }) => {
try {
// Get the full registry with password from database
const registryData = await db.query.registry.findFirst({
where: eq(registry.registryId, input.registryId ?? ""),
});

View File

@@ -3,16 +3,31 @@ import {
removeRollbackById,
rollback,
} from "@dokploy/server";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import { apiFindOneRollback } from "@/server/db/schema";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const rollbackRouter = createTRPCRouter({
delete: protectedProcedure
.input(apiFindOneRollback)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
return removeRollbackById(input.rollbackId);
const rb = await findRollbackById(input.rollbackId);
const serviceId = rb.deployment.applicationId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
deployment: ["create"],
});
}
const result = await removeRollbackById(input.rollbackId);
await audit(ctx, {
action: "delete",
resourceType: "deployment",
resourceId: input.rollbackId,
});
return result;
} catch (error) {
const message =
error instanceof Error
@@ -28,17 +43,20 @@ export const rollbackRouter = createTRPCRouter({
.input(apiFindOneRollback)
.mutation(async ({ input, ctx }) => {
try {
const currentRollback = await findRollbackById(input.rollbackId);
if (
currentRollback?.deployment?.application?.environment?.project
.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to rollback this deployment",
const rb = await findRollbackById(input.rollbackId);
const serviceId = rb.deployment.applicationId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
deployment: ["create"],
});
}
return await rollback(input.rollbackId);
const result = await rollback(input.rollbackId);
await audit(ctx, {
action: "restore",
resourceType: "deployment",
resourceId: input.rollbackId,
});
return result;
} catch (error) {
console.error(error);
throw new TRPCError({

View File

@@ -16,12 +16,20 @@ import {
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { removeJob, schedule } from "@/server/utils/backup";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const scheduleRouter = createTRPCRouter({
create: protectedProcedure
.input(createScheduleSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const serviceId = input.applicationId || input.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
schedule: ["create"],
});
}
const newSchedule = await createSchedule(input);
if (newSchedule?.enabled) {
@@ -36,12 +44,26 @@ export const scheduleRouter = createTRPCRouter({
scheduleJob(newSchedule);
}
}
await audit(ctx, {
action: "create",
resourceType: "schedule",
resourceId: newSchedule?.scheduleId,
resourceName: newSchedule?.name,
});
return newSchedule;
}),
update: protectedProcedure
.input(updateScheduleSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const existingSchedule = await findScheduleById(input.scheduleId);
const serviceId =
existingSchedule.applicationId || existingSchedule.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
schedule: ["update"],
});
}
const updatedSchedule = await updateSchedule(input);
if (IS_CLOUD) {
@@ -67,24 +89,42 @@ export const scheduleRouter = createTRPCRouter({
removeScheduleJob(updatedSchedule.scheduleId);
}
}
await audit(ctx, {
action: "update",
resourceType: "schedule",
resourceId: updatedSchedule.scheduleId,
resourceName: updatedSchedule.name,
});
return updatedSchedule;
}),
delete: protectedProcedure
.input(z.object({ scheduleId: z.string() }))
.mutation(async ({ input }) => {
const schedule = await findScheduleById(input.scheduleId);
.mutation(async ({ input, ctx }) => {
const scheduleItem = await findScheduleById(input.scheduleId);
const serviceId = scheduleItem.applicationId || scheduleItem.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
schedule: ["delete"],
});
}
await deleteSchedule(input.scheduleId);
if (IS_CLOUD) {
await removeJob({
cronSchedule: schedule.cronExpression,
scheduleId: schedule.scheduleId,
cronSchedule: scheduleItem.cronExpression,
scheduleId: scheduleItem.scheduleId,
type: "schedule",
});
} else {
removeScheduleJob(schedule.scheduleId);
removeScheduleJob(scheduleItem.scheduleId);
}
await audit(ctx, {
action: "delete",
resourceType: "schedule",
resourceId: scheduleItem.scheduleId,
resourceName: scheduleItem.name,
});
return true;
}),
@@ -100,7 +140,15 @@ export const scheduleRouter = createTRPCRouter({
]),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
if (
input.scheduleType === "application" ||
input.scheduleType === "compose"
) {
await checkServicePermissionAndAccess(ctx, input.id, {
schedule: ["read"],
});
}
const where = {
application: eq(schedules.applicationId, input.id),
compose: eq(schedules.composeId, input.id),
@@ -122,15 +170,34 @@ export const scheduleRouter = createTRPCRouter({
one: protectedProcedure
.input(z.object({ scheduleId: z.string() }))
.query(async ({ input }) => {
return await findScheduleById(input.scheduleId);
.query(async ({ input, ctx }) => {
const schedule = await findScheduleById(input.scheduleId);
const serviceId = schedule.applicationId || schedule.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
schedule: ["read"],
});
}
return schedule;
}),
runManually: protectedProcedure
.input(z.object({ scheduleId: z.string().min(1) }))
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const scheduleItem = await findScheduleById(input.scheduleId);
const serviceId = scheduleItem.applicationId || scheduleItem.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
schedule: ["create"],
});
}
try {
await runCommand(input.scheduleId);
await audit(ctx, {
action: "run",
resourceType: "schedule",
resourceId: input.scheduleId,
});
return true;
} catch (error) {
throw new TRPCError({

View File

@@ -1,80 +1,74 @@
import {
createSecurity,
deleteSecurityById,
findApplicationById,
findSecurityById,
updateSecurityById,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateSecurity,
apiFindOneSecurity,
apiUpdateSecurity,
} from "@/server/db/schema";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const securityRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateSecurity)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return await createSecurity(input);
await checkServicePermissionAndAccess(ctx, input.applicationId, {
service: ["create"],
});
await createSecurity(input);
await audit(ctx, {
action: "create",
resourceType: "security",
resourceId: input.applicationId,
resourceName: input.username,
});
return true;
}),
one: protectedProcedure
.input(apiFindOneSecurity)
.query(async ({ input, ctx }) => {
const security = await findSecurityById(input.securityId);
const application = await findApplicationById(security.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
await checkServicePermissionAndAccess(ctx, security.applicationId, {
service: ["read"],
});
return security;
}),
delete: protectedProcedure
.input(apiFindOneSecurity)
.mutation(async ({ input, ctx }) => {
const security = await findSecurityById(input.securityId);
const application = await findApplicationById(security.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return await deleteSecurityById(input.securityId);
await checkServicePermissionAndAccess(ctx, security.applicationId, {
service: ["delete"],
});
const result = await deleteSecurityById(input.securityId);
await audit(ctx, {
action: "delete",
resourceType: "security",
resourceId: input.securityId,
});
return result;
}),
update: protectedProcedure
.input(apiUpdateSecurity)
.mutation(async ({ input, ctx }) => {
const security = await findSecurityById(input.securityId);
const application = await findApplicationById(security.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this application",
});
}
return await updateSecurityById(input.securityId, input);
await checkServicePermissionAndAccess(ctx, security.applicationId, {
service: ["create"],
});
const result = await updateSecurityById(input.securityId, input);
await audit(ctx, {
action: "update",
resourceType: "security",
resourceId: input.securityId,
});
return result;
}),
});

View File

@@ -21,7 +21,12 @@ import { observable } from "@trpc/server/observable";
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
import { z } from "zod";
import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
createTRPCRouter,
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import {
apiCreateServer,
apiFindOneServer,
@@ -40,7 +45,7 @@ import {
} from "@/server/db/schema";
export const serverRouter = createTRPCRouter({
create: protectedProcedure
create: withPermission("server", "create")
.input(apiCreateServer)
.mutation(async ({ ctx, input }) => {
try {
@@ -56,6 +61,12 @@ export const serverRouter = createTRPCRouter({
input,
ctx.session.activeOrganizationId,
);
await audit(ctx, {
action: "create",
resourceType: "server",
resourceId: project.serverId,
resourceName: project.name,
});
return project;
} catch (error) {
throw new TRPCError({
@@ -66,7 +77,7 @@ export const serverRouter = createTRPCRouter({
}
}),
one: protectedProcedure
one: withPermission("server", "read")
.input(apiFindOneServer)
.query(async ({ input, ctx }) => {
const server = await findServerById(input.serverId);
@@ -79,14 +90,14 @@ export const serverRouter = createTRPCRouter({
return server;
}),
getDefaultCommand: protectedProcedure
getDefaultCommand: withPermission("server", "read")
.input(apiFindOneServer)
.query(async ({ input }) => {
const server = await findServerById(input.serverId);
const isBuildServer = server.serverType === "build";
return defaultCommand(isBuildServer);
}),
all: protectedProcedure.query(async ({ ctx }) => {
all: withPermission("server", "read").query(async ({ ctx }) => {
const result = await db
.select({
...getTableColumns(server),
@@ -118,7 +129,7 @@ export const serverRouter = createTRPCRouter({
return servers.length ?? 0;
}),
withSSHKey: protectedProcedure.query(async ({ ctx }) => {
withSSHKey: withPermission("server", "read").query(async ({ ctx }) => {
const result = await db.query.server.findMany({
orderBy: desc(server.createdAt),
where: IS_CLOUD
@@ -136,7 +147,7 @@ export const serverRouter = createTRPCRouter({
});
return result;
}),
buildServers: protectedProcedure.query(async ({ ctx }) => {
buildServers: withPermission("server", "read").query(async ({ ctx }) => {
const result = await db.query.server.findMany({
orderBy: desc(server.createdAt),
where: IS_CLOUD
@@ -154,7 +165,7 @@ export const serverRouter = createTRPCRouter({
});
return result;
}),
setup: protectedProcedure
setup: withPermission("server", "create")
.input(apiFindOneServer)
.mutation(async ({ input, ctx }) => {
try {
@@ -166,12 +177,18 @@ export const serverRouter = createTRPCRouter({
});
}
const currentServer = await serverSetup(input.serverId);
await audit(ctx, {
action: "update",
resourceType: "server",
resourceId: input.serverId,
resourceName: server.name,
});
return currentServer;
} catch (error) {
throw error;
}
}),
setupWithLogs: protectedProcedure
setupWithLogs: withPermission("server", "create")
.meta({
openapi: {
path: "/deploy/server-with-logs",
@@ -199,7 +216,7 @@ export const serverRouter = createTRPCRouter({
throw error;
}
}),
validate: protectedProcedure
validate: withPermission("server", "read")
.input(apiFindOneServer)
.query(async ({ input, ctx }) => {
try {
@@ -245,7 +262,7 @@ export const serverRouter = createTRPCRouter({
}
}),
security: protectedProcedure
security: withPermission("server", "read")
.input(apiFindOneServer)
.query(async ({ input, ctx }) => {
try {
@@ -295,7 +312,7 @@ export const serverRouter = createTRPCRouter({
});
}
}),
setupMonitoring: protectedProcedure
setupMonitoring: withPermission("server", "create")
.input(apiUpdateServerMonitoring)
.mutation(async ({ input, ctx }) => {
try {
@@ -332,22 +349,21 @@ export const serverRouter = createTRPCRouter({
},
});
const currentServer = await setupMonitoring(input.serverId);
await audit(ctx, {
action: "update",
resourceType: "server",
resourceId: input.serverId,
resourceName: server.name,
});
return currentServer;
} catch (error) {
throw error;
}
}),
remove: protectedProcedure
remove: withPermission("server", "delete")
.input(apiRemoveServer)
.mutation(async ({ input, ctx }) => {
try {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this server",
});
}
const activeServers = await haveActiveServices(input.serverId);
if (activeServers) {
@@ -357,6 +373,12 @@ export const serverRouter = createTRPCRouter({
});
}
const currentServer = await findServerById(input.serverId);
await audit(ctx, {
action: "delete",
resourceType: "server",
resourceId: currentServer.serverId,
resourceName: currentServer.name,
});
await removeDeploymentsByServerId(currentServer);
await deleteServer(input.serverId);
@@ -371,7 +393,7 @@ export const serverRouter = createTRPCRouter({
throw error;
}
}),
update: protectedProcedure
update: withPermission("server", "create")
.input(apiUpdateServer)
.mutation(async ({ input, ctx }) => {
try {
@@ -393,6 +415,12 @@ export const serverRouter = createTRPCRouter({
...input,
});
await audit(ctx, {
action: "update",
resourceType: "server",
resourceId: input.serverId,
resourceName: server.name,
});
return currentServer;
} catch (error) {
throw error;
@@ -414,7 +442,7 @@ export const serverRouter = createTRPCRouter({
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
}),
getServerMetrics: protectedProcedure
getServerMetrics: withPermission("monitoring", "read")
.input(
z.object({
url: z.string(),

View File

@@ -1,8 +1,10 @@
import {
CLEANUP_CRON_JOB,
canAccessToTraefikFiles,
checkGPUStatus,
checkPortInUse,
checkPostgresHealth,
checkRedisHealth,
checkTraefikHealth,
cleanupAll,
cleanupAllBackground,
cleanupBuilders,
@@ -46,12 +48,14 @@ import {
writeTraefikSetup,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { checkPermission } from "@dokploy/server/services/permission";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { TRPCError } from "@trpc/server";
import { eq, sql } from "drizzle-orm";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { parse, stringify } from "yaml";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import {
apiAssignDomain,
apiEnableDashboard,
@@ -84,14 +88,19 @@ export const settingsRouter = createTRPCRouter({
const settings = await getWebServerSettings();
return settings;
}),
reloadServer: adminProcedure.mutation(async () => {
reloadServer: adminProcedure.mutation(async ({ ctx }) => {
if (IS_CLOUD) {
return true;
}
await reloadDockerResource("dokploy", undefined, packageInfo.version);
await audit(ctx, {
action: "reload",
resourceType: "settings",
resourceName: "dokploy",
});
return true;
}),
cleanRedis: adminProcedure.mutation(async () => {
cleanRedis: adminProcedure.mutation(async ({ ctx }) => {
if (IS_CLOUD) {
return true;
}
@@ -107,36 +116,56 @@ export const settingsRouter = createTRPCRouter({
const redisContainerId = containerId.trim();
await execAsync(`docker exec -i ${redisContainerId} redis-cli flushall`);
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "clean-redis",
});
return true;
}),
reloadRedis: adminProcedure.mutation(async () => {
reloadRedis: adminProcedure.mutation(async ({ ctx }) => {
if (IS_CLOUD) {
return true;
}
await reloadDockerResource("dokploy-redis");
await audit(ctx, {
action: "reload",
resourceType: "settings",
resourceName: "dokploy-redis",
});
return true;
}),
cleanAllDeploymentQueue: adminProcedure.mutation(async () => {
cleanAllDeploymentQueue: adminProcedure.mutation(async ({ ctx }) => {
if (IS_CLOUD) {
return true;
}
return cleanAllDeploymentQueue();
const result = cleanAllDeploymentQueue();
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "clean-deployment-queue",
});
return result;
}),
reloadTraefik: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
// Run in background so the request returns immediately; avoids proxy timeouts.
void reloadDockerResource("dokploy-traefik", input?.serverId).catch(
(err) => {
console.error("reloadTraefik background:", err);
},
);
await audit(ctx, {
action: "reload",
resourceType: "settings",
resourceName: "dokploy-traefik",
});
return true;
}),
toggleDashboard: adminProcedure
.input(apiEnableDashboard)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const ports = await readPorts("dokploy-traefik", input.serverId);
const env = await readEnvironmentVariables(
"dokploy-traefik",
@@ -175,70 +204,112 @@ export const settingsRouter = createTRPCRouter({
}).catch((err) => {
console.error("toggleDashboard background writeTraefikSetup:", err);
});
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "toggle-dashboard",
});
return true;
}),
cleanUnusedImages: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
await cleanupImages(input?.serverId);
await audit(ctx, {
action: "delete",
resourceType: "settings",
resourceName: "clean-unused-images",
});
return true;
}),
cleanUnusedVolumes: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
await cleanupVolumes(input?.serverId);
await audit(ctx, {
action: "delete",
resourceType: "settings",
resourceName: "clean-unused-volumes",
});
return true;
}),
cleanStoppedContainers: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
await cleanupContainers(input?.serverId);
await audit(ctx, {
action: "delete",
resourceType: "settings",
resourceName: "clean-stopped-containers",
});
return true;
}),
cleanDockerBuilder: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
await cleanupBuilders(input?.serverId);
await audit(ctx, {
action: "delete",
resourceType: "settings",
resourceName: "clean-docker-builder",
});
}),
cleanDockerPrune: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
await cleanupSystem(input?.serverId);
await cleanupBuilders(input?.serverId);
await audit(ctx, {
action: "delete",
resourceType: "settings",
resourceName: "clean-docker-prune",
});
return true;
}),
cleanAll: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
// Execute cleanup in background and return immediately to avoid gateway timeouts
const result = await cleanupAllBackground(input?.serverId);
await audit(ctx, {
action: "delete",
resourceType: "settings",
resourceName: "clean-all",
});
return result;
}),
cleanMonitoring: adminProcedure.mutation(async () => {
cleanMonitoring: adminProcedure.mutation(async ({ ctx }) => {
if (IS_CLOUD) {
return true;
}
const { MONITORING_PATH } = paths();
await recreateDirectory(MONITORING_PATH);
await audit(ctx, {
action: "delete",
resourceType: "settings",
resourceName: "clean-monitoring",
});
return true;
}),
saveSSHPrivateKey: adminProcedure
.input(apiSaveSSHKey)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
return true;
}
await updateWebServerSettings({
sshPrivateKey: input.sshPrivateKey,
});
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "ssh-private-key",
});
return true;
}),
assignDomainServer: adminProcedure
.input(apiAssignDomain)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
return true;
}
@@ -261,15 +332,25 @@ export const settingsRouter = createTRPCRouter({
updateLetsEncryptEmail(input.letsEncryptEmail);
}
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "assign-domain-server",
});
return settings;
}),
cleanSSHPrivateKey: adminProcedure.mutation(async () => {
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => {
if (IS_CLOUD) {
return true;
}
await updateWebServerSettings({
sshPrivateKey: null,
});
await audit(ctx, {
action: "delete",
resourceType: "settings",
resourceName: "ssh-private-key",
});
return true;
}),
updateDockerCleanup: adminProcedure
@@ -349,6 +430,11 @@ export const settingsRouter = createTRPCRouter({
}
}
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "docker-cleanup",
});
return true;
}),
@@ -362,11 +448,16 @@ export const settingsRouter = createTRPCRouter({
updateTraefikConfig: adminProcedure
.input(apiTraefikConfig)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
return true;
}
writeMainConfig(input.traefikConfig);
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "traefik-config",
});
return true;
}),
@@ -379,11 +470,16 @@ export const settingsRouter = createTRPCRouter({
}),
updateWebServerTraefikConfig: adminProcedure
.input(apiTraefikConfig)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
return true;
}
writeConfig("dokploy", input.traefikConfig);
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "web-server-traefik-config",
});
return true;
}),
@@ -397,11 +493,16 @@ export const settingsRouter = createTRPCRouter({
updateMiddlewareTraefikConfig: adminProcedure
.input(apiTraefikConfig)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
return true;
}
writeConfig("middlewares", input.traefikConfig);
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "middleware-traefik-config",
});
return true;
}),
getUpdateData: protectedProcedure.mutation(async () => {
@@ -411,7 +512,7 @@ export const settingsRouter = createTRPCRouter({
return await getUpdateData(packageInfo.version);
}),
updateServer: adminProcedure.mutation(async () => {
updateServer: adminProcedure.mutation(async ({ ctx }) => {
if (IS_CLOUD) {
return true;
}
@@ -426,6 +527,11 @@ export const settingsRouter = createTRPCRouter({
`dokploy/dokploy:${data.latestVersion}`,
"dokploy",
]);
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "dokploy-version",
});
}
return true;
@@ -441,16 +547,7 @@ export const settingsRouter = createTRPCRouter({
.input(apiServerSchema)
.query(async ({ ctx, input }) => {
try {
if (ctx.user.role === "member") {
const canAccess = await canAccessToTraefikFiles(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (!canAccess) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
await checkPermission(ctx, { traefikFiles: ["read"] });
const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId);
const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId);
return result || [];
@@ -462,37 +559,24 @@ export const settingsRouter = createTRPCRouter({
updateTraefikFile: protectedProcedure
.input(apiModifyTraefikConfig)
.mutation(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
const canAccess = await canAccessToTraefikFiles(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (!canAccess) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
await checkPermission(ctx, { traefikFiles: ["write"] });
await writeTraefikConfigInPath(
input.path,
input.traefikConfig,
input?.serverId,
);
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "traefik-file",
});
return true;
}),
readTraefikFile: protectedProcedure
.input(apiReadTraefikConfig)
.query(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
const canAccess = await canAccessToTraefikFiles(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (!canAccess) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
await checkPermission(ctx, { traefikFiles: ["read"] });
if (input.serverId) {
const server = await findServerById(input.serverId);
@@ -517,13 +601,18 @@ export const settingsRouter = createTRPCRouter({
serverIp: z.string(),
}),
)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
return true;
}
const settings = await updateWebServerSettings({
serverIp: input.serverIp,
});
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "server-ip",
});
return settings;
}),
@@ -610,7 +699,7 @@ export const settingsRouter = createTRPCRouter({
writeTraefikEnv: adminProcedure
.input(z.object({ env: z.string(), serverId: z.string().optional() }))
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const envs = prepareEnvironmentVariables(input.env);
const ports = await readPorts("dokploy-traefik", input?.serverId);
@@ -622,6 +711,11 @@ export const settingsRouter = createTRPCRouter({
}).catch((err) => {
console.error("writeTraefikEnv background writeTraefikSetup:", err);
});
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "traefik-env",
});
return true;
}),
haveTraefikDashboardPortEnabled: adminProcedure
@@ -715,7 +809,7 @@ export const settingsRouter = createTRPCRouter({
enable: z.boolean(),
}),
)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
return true;
}
@@ -742,7 +836,11 @@ export const settingsRouter = createTRPCRouter({
}
writeMainConfig(stringify(currentConfig));
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "toggle-requests",
});
return true;
}),
isCloud: publicProcedure.query(async () => {
@@ -769,19 +867,41 @@ export const settingsRouter = createTRPCRouter({
throw error;
}
}),
checkInfrastructureHealth: adminProcedure.query(async () => {
if (IS_CLOUD) {
return {
postgres: { status: "healthy" as const },
redis: { status: "healthy" as const },
traefik: { status: "healthy" as const },
};
}
const [postgres, redis, traefik] = await Promise.all([
checkPostgresHealth(),
checkRedisHealth(),
checkTraefikHealth(),
]);
return { postgres, redis, traefik };
}),
setupGPU: adminProcedure
.input(
z.object({
serverId: z.string().optional(),
}),
)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD && !input.serverId) {
throw new Error("Select a server to enable the GPU Setup");
}
try {
await setupGPUSupport(input.serverId);
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "setup-gpu",
});
return { success: true };
} catch (error) {
console.error("GPU Setup Error:", error);
@@ -835,7 +955,7 @@ export const settingsRouter = createTRPCRouter({
),
}),
)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
@@ -873,6 +993,11 @@ export const settingsRouter = createTRPCRouter({
err,
);
});
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "traefik-ports",
});
return true;
} catch (error) {
throw new TRPCError({
@@ -897,14 +1022,22 @@ export const settingsRouter = createTRPCRouter({
cronExpression: z.string().nullable(),
}),
)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
return true;
}
let result: boolean;
if (input.cronExpression) {
return startLogCleanup(input.cronExpression);
result = await startLogCleanup(input.cronExpression);
} else {
result = await stopLogCleanup();
}
return stopLogCleanup();
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "log-cleanup",
});
return result;
}),
getLogCleanupStatus: protectedProcedure.query(async () => {

View File

@@ -8,7 +8,8 @@ import {
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, withPermission } from "@/server/api/trpc";
import {
apiCreateSshKey,
apiFindOneSshKey,
@@ -19,7 +20,7 @@ import {
} from "@/server/db/schema";
export const sshRouter = createTRPCRouter({
create: protectedProcedure
create: withPermission("sshKeys", "create")
.input(apiCreateSshKey)
.mutation(async ({ input, ctx }) => {
try {
@@ -27,6 +28,11 @@ export const sshRouter = createTRPCRouter({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "create",
resourceType: "sshKey",
resourceName: input.name,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -35,7 +41,7 @@ export const sshRouter = createTRPCRouter({
});
}
}),
remove: protectedProcedure
remove: withPermission("sshKeys", "delete")
.input(apiRemoveSshKey)
.mutation(async ({ input, ctx }) => {
try {
@@ -47,12 +53,18 @@ export const sshRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "delete",
resourceType: "sshKey",
resourceId: sshKey.sshKeyId,
resourceName: sshKey.name,
});
return await removeSSHKeyById(input.sshKeyId);
} catch (error) {
throw error;
}
}),
one: protectedProcedure
one: withPermission("sshKeys", "read")
.input(apiFindOneSshKey)
.query(async ({ input, ctx }) => {
const sshKey = await findSSHKeyById(input.sshKeyId);
@@ -65,18 +77,18 @@ export const sshRouter = createTRPCRouter({
}
return sshKey;
}),
all: protectedProcedure.query(async ({ ctx }) => {
all: withPermission("sshKeys", "read").query(async ({ ctx }) => {
return await db.query.sshKeys.findMany({
where: eq(sshKeys.organizationId, ctx.session.activeOrganizationId),
orderBy: desc(sshKeys.createdAt),
});
}),
generate: protectedProcedure
generate: withPermission("sshKeys", "read")
.input(apiGenerateSSHKey)
.mutation(async ({ input }) => {
return await generateSSHKey(input.type);
}),
update: protectedProcedure
update: withPermission("sshKeys", "create")
.input(apiUpdateSshKey)
.mutation(async ({ input, ctx }) => {
try {
@@ -87,7 +99,14 @@ export const sshRouter = createTRPCRouter({
message: "You are not allowed to update this SSH key",
});
}
return await updateSSHKeyById(input);
const result = await updateSSHKeyById(input);
await audit(ctx, {
action: "update",
resourceType: "sshKey",
resourceId: sshKey.sshKeyId,
resourceName: sshKey.name,
});
return result;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",

View File

@@ -21,7 +21,12 @@ import {
STARTUP_PRODUCT_ID,
WEBSITE_URL,
} from "@/server/utils/stripe";
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
withPermission,
} from "../trpc";
export const stripeRouter = createTRPCRouter({
/** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */
@@ -314,16 +319,18 @@ export const stripeRouter = createTRPCRouter({
return { ok: true };
}),
canCreateMoreServers: adminProcedure.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
const servers = await findServersByUserId(user.id);
canCreateMoreServers: withPermission("server", "create").query(
async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
const servers = await findServersByUserId(user.id);
if (!IS_CLOUD) {
return true;
}
if (!IS_CLOUD) {
return true;
}
return servers.length < user.serversQuantity;
}),
return servers.length < user.serversQuantity;
},
),
getInvoices: adminProcedure.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);

View File

@@ -1,58 +1,38 @@
import {
findServerById,
getApplicationInfo,
getNodeApplications,
getNodeInfo,
getSwarmNodes,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { createTRPCRouter, withPermission } from "../trpc";
import { containerIdRegex } from "./docker";
export const swarmRouter = createTRPCRouter({
getNodes: protectedProcedure
getNodes: withPermission("server", "read")
.input(
z.object({
serverId: z.string().optional(),
}),
)
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
.query(async ({ input }) => {
return await getSwarmNodes(input.serverId);
}),
getNodeInfo: protectedProcedure
getNodeInfo: withPermission("server", "read")
.input(z.object({ nodeId: z.string(), serverId: z.string().optional() }))
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
.query(async ({ input }) => {
return await getNodeInfo(input.nodeId, input.serverId);
}),
getNodeApps: protectedProcedure
getNodeApps: withPermission("server", "read")
.input(
z.object({
serverId: z.string().optional(),
}),
)
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
.query(async ({ input }) => {
return getNodeApplications(input.serverId);
}),
getAppInfos: protectedProcedure
getAppInfos: withPermission("server", "read")
.meta({
openapi: {
path: "/drop-deployment",
@@ -71,13 +51,7 @@ export const swarmRouter = createTRPCRouter({
serverId: z.string().optional(),
}),
)
.query(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
.query(async ({ input }) => {
return await getApplicationInfo(input.appName, input.serverId);
}),
});

View File

@@ -22,15 +22,21 @@ import {
invitation,
member,
} from "@dokploy/server/db/schema";
import {
hasPermission,
resolvePermissions,
} from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { and, asc, eq, gt } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
publicProcedure,
withPermission,
} from "../trpc";
const apiCreateApiKey = z.object({
@@ -51,7 +57,7 @@ const apiCreateApiKey = z.object({
});
export const userRouter = createTRPCRouter({
all: adminProcedure.query(async ({ ctx }) => {
all: withPermission("member", "read").query(async ({ ctx }) => {
return await db.query.member.findMany({
where: eq(member.organizationId, ctx.session.activeOrganizationId),
with: {
@@ -87,21 +93,28 @@ export const userRouter = createTRPCRouter({
// Allow access if:
// 1. User is requesting their own information
// 2. User has owner role (admin permissions) AND user is in the same organization
// 2. User is owner/admin
// 3. User has member.update permission (custom roles managing permissions)
if (
memberResult.userId !== ctx.user.id &&
ctx.user.role !== "owner" &&
ctx.user.role !== "admin"
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this user",
});
const canUpdate = await hasPermission(ctx, { member: ["update"] });
if (!canUpdate) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this user",
});
}
}
return memberResult;
}),
session: protectedProcedure.query(async ({ ctx }) => {
session: publicProcedure.query(async ({ ctx }) => {
if (!ctx.user || !ctx.session || !ctx.session.activeOrganizationId) {
return null;
}
return {
user: {
id: ctx.user.id,
@@ -128,6 +141,9 @@ export const userRouter = createTRPCRouter({
return memberResult;
}),
getPermissions: protectedProcedure.query(async ({ ctx }) => {
return resolvePermissions(ctx);
}),
haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
if (!IS_CLOUD) {
return false;
@@ -163,19 +179,21 @@ export const userRouter = createTRPCRouter({
return memberResult?.user;
}),
getServerMetrics: protectedProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, ctx.user.id),
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
),
with: {
user: true,
},
});
getServerMetrics: withPermission("monitoring", "read").query(
async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, ctx.user.id),
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
),
with: {
user: true,
},
});
return memberResult?.user;
}),
return memberResult?.user;
},
),
update: protectedProcedure
.input(apiUpdateUser)
.mutation(async ({ input, ctx }) => {
@@ -210,7 +228,14 @@ export const userRouter = createTRPCRouter({
}
try {
return await updateUser(ctx.user.id, input);
const result = await updateUser(ctx.user.id, input);
await audit(ctx, {
action: "update",
resourceType: "user",
resourceId: ctx.user.id,
resourceName: ctx.user.email,
});
return result;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -224,15 +249,17 @@ export const userRouter = createTRPCRouter({
.query(async ({ input }) => {
return await getUserByToken(input.token);
}),
getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
const settings = await getWebServerSettings();
return {
serverIp: settings?.serverIp,
enabledFeatures: user.enablePaidFeatures,
metricsConfig: settings?.metricsConfig,
};
}),
getMetricsToken: withPermission("monitoring", "read").query(
async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
const settings = await getWebServerSettings();
return {
serverIp: settings?.serverIp,
enabledFeatures: user.enablePaidFeatures,
metricsConfig: settings?.metricsConfig,
};
},
),
remove: protectedProcedure
.input(
z.object({
@@ -294,9 +321,15 @@ export const userRouter = createTRPCRouter({
});
}
return await removeUserById(input.userId);
const result = await removeUserById(input.userId);
await audit(ctx, {
action: "delete",
resourceType: "user",
resourceId: input.userId,
});
return result;
}),
assignPermissions: adminProcedure
assignPermissions: withPermission("member", "update")
.input(apiAssignPermissions)
.mutation(async ({ input, ctx }) => {
try {
@@ -327,6 +360,12 @@ export const userRouter = createTRPCRouter({
),
),
);
await audit(ctx, {
action: "update",
resourceType: "user",
resourceId: input.id,
metadata: { permissions: rest },
});
} catch (error) {
throw error;
}
@@ -344,7 +383,7 @@ export const userRouter = createTRPCRouter({
});
}),
getContainerMetrics: protectedProcedure
getContainerMetrics: withPermission("monitoring", "read")
.input(
z.object({
url: z.string(),
@@ -426,7 +465,7 @@ export const userRouter = createTRPCRouter({
});
}
if (apiKeyToDelete.userId !== ctx.user.id) {
if (apiKeyToDelete.referenceId !== ctx.user.id) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this API key",
@@ -434,6 +473,12 @@ export const userRouter = createTRPCRouter({
}
await db.delete(apikey).where(eq(apikey.id, input.apiKeyId));
await audit(ctx, {
action: "delete",
resourceType: "user",
resourceId: input.apiKeyId,
resourceName: apiKeyToDelete.name || undefined,
});
return true;
} catch (error) {
throw error;
@@ -461,6 +506,12 @@ export const userRouter = createTRPCRouter({
}
const apiKey = await createApiKey(ctx.user.id, input);
await audit(ctx, {
action: "create",
resourceType: "user",
resourceId: apiKey.id,
resourceName: input.name,
});
return apiKey;
}),
@@ -505,7 +556,7 @@ export const userRouter = createTRPCRouter({
return organizations.length;
}),
sendInvitation: adminProcedure
sendInvitation: withPermission("member", "create")
.input(
z.object({
invitationId: z.string().min(1),
@@ -571,6 +622,13 @@ export const userRouter = createTRPCRouter({
console.log(error);
throw error;
}
await audit(ctx, {
action: "create",
resourceType: "user",
resourceId: input.invitationId,
resourceName: currentInvitation?.email || "",
metadata: { type: "sendInvitation" },
});
return inviteLink;
}),
});

View File

@@ -23,8 +23,10 @@ import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const volumeBackupsRouter = createTRPCRouter({
list: protectedProcedure
@@ -42,7 +44,10 @@ export const volumeBackupsRouter = createTRPCRouter({
]),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.id, {
volumeBackup: ["read"],
});
return await db.query.volumeBackups.findMany({
where: eq(volumeBackups[`${input.volumeBackupType}Id`], input.id),
with: {
@@ -59,7 +64,20 @@ export const volumeBackupsRouter = createTRPCRouter({
}),
create: protectedProcedure
.input(createVolumeBackupSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const serviceId =
input.applicationId ||
input.postgresId ||
input.mysqlId ||
input.mariadbId ||
input.mongoId ||
input.redisId ||
input.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volumeBackup: ["create"],
});
}
const newVolumeBackup = await createVolumeBackup(input);
if (newVolumeBackup?.enabled) {
@@ -73,6 +91,11 @@ export const volumeBackupsRouter = createTRPCRouter({
await scheduleVolumeBackup(newVolumeBackup.volumeBackupId);
}
}
await audit(ctx, {
action: "create",
resourceType: "volumeBackup",
resourceId: newVolumeBackup?.volumeBackupId,
});
return newVolumeBackup;
}),
one: protectedProcedure
@@ -81,8 +104,22 @@ export const volumeBackupsRouter = createTRPCRouter({
volumeBackupId: z.string().min(1),
}),
)
.query(async ({ input }) => {
return await findVolumeBackupById(input.volumeBackupId);
.query(async ({ input, ctx }) => {
const vb = await findVolumeBackupById(input.volumeBackupId);
const serviceId =
vb.applicationId ||
vb.postgresId ||
vb.mysqlId ||
vb.mariadbId ||
vb.mongoId ||
vb.redisId ||
vb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volumeBackup: ["read"],
});
}
return vb;
}),
delete: protectedProcedure
.input(
@@ -90,12 +127,46 @@ export const volumeBackupsRouter = createTRPCRouter({
volumeBackupId: z.string().min(1),
}),
)
.mutation(async ({ input }) => {
return await removeVolumeBackup(input.volumeBackupId);
.mutation(async ({ input, ctx }) => {
const vb = await findVolumeBackupById(input.volumeBackupId);
const serviceId =
vb.applicationId ||
vb.postgresId ||
vb.mysqlId ||
vb.mariadbId ||
vb.mongoId ||
vb.redisId ||
vb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volumeBackup: ["delete"],
});
}
const result = await removeVolumeBackup(input.volumeBackupId);
await audit(ctx, {
action: "delete",
resourceType: "volumeBackup",
resourceId: input.volumeBackupId,
});
return result;
}),
update: protectedProcedure
.input(updateVolumeBackupSchema)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const existingVb = await findVolumeBackupById(input.volumeBackupId);
const serviceId =
existingVb.applicationId ||
existingVb.postgresId ||
existingVb.mysqlId ||
existingVb.mariadbId ||
existingVb.mongoId ||
existingVb.redisId ||
existingVb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volumeBackup: ["update"],
});
}
const updatedVolumeBackup = await updateVolumeBackup(
input.volumeBackupId,
input,
@@ -130,20 +201,45 @@ export const volumeBackupsRouter = createTRPCRouter({
removeVolumeBackupJob(updatedVolumeBackup.volumeBackupId);
}
}
await audit(ctx, {
action: "update",
resourceType: "volumeBackup",
resourceId: updatedVolumeBackup.volumeBackupId,
});
return updatedVolumeBackup;
}),
runManually: protectedProcedure
.input(z.object({ volumeBackupId: z.string().min(1) }))
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const vb = await findVolumeBackupById(input.volumeBackupId);
const serviceId =
vb.applicationId ||
vb.postgresId ||
vb.mysqlId ||
vb.mariadbId ||
vb.mongoId ||
vb.redisId ||
vb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volumeBackup: ["create"],
});
}
try {
return await runVolumeBackup(input.volumeBackupId);
const result = await runVolumeBackup(input.volumeBackupId);
await audit(ctx, {
action: "run",
resourceType: "volumeBackup",
resourceId: input.volumeBackupId,
});
return result;
} catch (error) {
console.error(error);
return false;
}
}),
restoreVolumeBackupWithLogs: protectedProcedure
restoreVolumeBackupWithLogs: withPermission("volumeBackup", "restore")
.meta({
openapi: {
enabled: false,

View File

@@ -10,7 +10,9 @@
// import { getServerAuthSession } from "@/server/auth";
import { db } from "@dokploy/server/db";
import { hasValidLicense } from "@dokploy/server/index";
import type { statements } from "@dokploy/server/lib/access-control";
import { validateRequest } from "@dokploy/server/lib/auth";
import { checkPermission } from "@dokploy/server/services/permission";
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
import { initTRPC, TRPCError } from "@trpc/server";
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
@@ -18,6 +20,9 @@ import type { Session, User } from "better-auth";
import superjson from "superjson";
import { ZodError } from "zod";
type Resource = keyof typeof statements;
type ActionOf<R extends Resource> = (typeof statements)[R][number];
/**
* 1. CONTEXT
*
@@ -235,3 +240,26 @@ export const enterpriseProcedure = t.procedure.use(async ({ ctx, next }) => {
},
});
});
/**
* Permission-checked procedure factory.
*
* Verifies the caller has the required resource+action permission before the
* handler runs. Works for all role types:
* - owner / admin → always granted (static roles, no license needed)
* - member → legacy boolean fields (no license needed)
* - custom role → enterprise license verified automatically inside resolveRole
*
* Usage:
* create: withPermission("project", "create")
* .input(...)
* .mutation(async ({ ctx, input }) => { ... })
*/
export const withPermission = <R extends Resource>(
resource: R,
action: ActionOf<R>,
) =>
protectedProcedure.use(async ({ ctx, next }) => {
await checkPermission(ctx, { [resource]: [action] } as any);
return next();
});

View File

@@ -0,0 +1,31 @@
import { createAuditLog } from "@dokploy/server/services/proprietary/audit-log";
import type { AuditAction, AuditResourceType } from "@dokploy/server/db/schema";
interface AuditCtx {
user: { id: string; email: string; role: string };
session: { activeOrganizationId: string };
}
interface AuditEvent {
action: AuditAction;
resourceType: AuditResourceType;
resourceId?: string;
resourceName?: string;
metadata?: Record<string, unknown>;
}
/**
* Creates an audit log entry from a tRPC context.
* Extracts userId, userEmail, userRole and organizationId automatically.
*
* Usage:
* await audit(ctx, { action: "create", resourceType: "project", resourceName: "my-app" });
*/
export const audit = (ctx: AuditCtx, event: AuditEvent) =>
createAuditLog({
organizationId: ctx.session.activeOrganizationId,
userId: ctx.user.id,
userEmail: ctx.user.email,
userRole: ctx.user.role,
...event,
});