Merge branch 'canary' into feat/add-mattermost-notification-provider

This commit is contained in:
Mauricio Siu
2026-03-24 01:29:08 -06:00
298 changed files with 116790 additions and 28183 deletions

View File

@@ -16,6 +16,7 @@ import { gitProviderRouter } from "./routers/git-provider";
import { giteaRouter } from "./routers/gitea";
import { githubRouter } from "./routers/github";
import { gitlabRouter } from "./routers/gitlab";
import { libsqlRouter } from "./routers/libsql";
import { mariadbRouter } from "./routers/mariadb";
import { mongoRouter } from "./routers/mongo";
import { mountRouter } from "./routers/mount";
@@ -23,12 +24,15 @@ import { mysqlRouter } from "./routers/mysql";
import { notificationRouter } from "./routers/notification";
import { organizationRouter } from "./routers/organization";
import { patchRouter } from "./routers/patch";
import { licenseKeyRouter } from "./routers/proprietary/license-key";
import { ssoRouter } from "./routers/proprietary/sso";
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";
@@ -40,6 +44,7 @@ import { settingsRouter } from "./routers/settings";
import { sshRouter } from "./routers/ssh-key";
import { stripeRouter } from "./routers/stripe";
import { swarmRouter } from "./routers/swarm";
import { tagRouter } from "./routers/tag";
import { userRouter } from "./routers/user";
import { volumeBackupsRouter } from "./routers/volume-backups";
/**
@@ -50,47 +55,52 @@ import { volumeBackupsRouter } from "./routers/volume-backups";
export const appRouter = createTRPCRouter({
admin: adminRouter,
docker: dockerRouter,
project: projectRouter,
application: applicationRouter,
mysql: mysqlRouter,
postgres: postgresRouter,
redis: redisRouter,
mongo: mongoRouter,
mariadb: mariadbRouter,
compose: composeRouter,
user: userRouter,
domain: domainRouter,
destination: destinationRouter,
backup: backupRouter,
deployment: deploymentRouter,
previewDeployment: previewDeploymentRouter,
mounts: mountRouter,
certificates: certificateRouter,
settings: settingsRouter,
security: securityRouter,
redirects: redirectsRouter,
port: portRouter,
registry: registryRouter,
cluster: clusterRouter,
notification: notificationRouter,
sshKey: sshRouter,
gitProvider: gitProviderRouter,
gitea: giteaRouter,
bitbucket: bitbucketRouter,
gitlab: gitlabRouter,
certificates: certificateRouter,
cluster: clusterRouter,
compose: composeRouter,
deployment: deploymentRouter,
destination: destinationRouter,
docker: dockerRouter,
domain: domainRouter,
gitea: giteaRouter,
gitProvider: gitProviderRouter,
github: githubRouter,
gitlab: gitlabRouter,
libsql: libsqlRouter,
mariadb: mariadbRouter,
mongo: mongoRouter,
mounts: mountRouter,
mysql: mysqlRouter,
notification: notificationRouter,
port: portRouter,
postgres: postgresRouter,
previewDeployment: previewDeploymentRouter,
project: projectRouter,
redirects: redirectsRouter,
redis: redisRouter,
registry: registryRouter,
security: securityRouter,
server: serverRouter,
settings: settingsRouter,
sshKey: sshRouter,
stripe: stripeRouter,
swarm: swarmRouter,
user: userRouter,
ai: aiRouter,
organization: organizationRouter,
licenseKey: licenseKeyRouter,
sso: ssoRouter,
whitelabeling: whitelabelingRouter,
customRole: customRoleRouter,
auditLog: auditLogRouter,
schedule: scheduleRouter,
rollback: rollbackRouter,
volumeBackups: volumeBackupsRouter,
environment: environmentRouter,
tag: tagRouter,
patch: patchRouter,
});

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

@@ -3,6 +3,8 @@ import {
findBackupById,
findComposeByBackupId,
findComposeById,
findLibsqlByBackupId,
findLibsqlById,
findMariadbByBackupId,
findMariadbById,
findMongoByBackupId,
@@ -16,6 +18,7 @@ import {
keepLatestNBackups,
removeBackupById,
removeScheduleBackup,
runLibsqlBackup,
runMariadbBackup,
runMongoBackup,
runMySqlBackup,
@@ -36,6 +39,7 @@ import {
} from "@dokploy/server/utils/process/execAsync";
import {
restoreComposeBackup,
restoreLibsqlBackup,
restoreMariadbBackup,
restoreMongoBackup,
restoreMySqlBackup,
@@ -44,7 +48,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 +79,22 @@ 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.libsqlId ||
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) {
@@ -86,6 +108,8 @@ export const backupRouter = createTRPCRouter({
serverId = backup.mongo.serverId;
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
serverId = backup.mariadb.serverId;
} else if (databaseType === "libsql" && backup.libsql?.serverId) {
serverId = backup.libsql.serverId;
} else if (
backup.backupType === "compose" &&
backup.compose?.serverId
@@ -110,6 +134,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 +151,44 @@ 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.libsqlId ||
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.libsqlId ||
existing.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
backup: ["update"],
});
}
await updateBackupById(input.backupId, input);
const backup = await findBackupById(input.backupId);
@@ -156,6 +214,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 +230,22 @@ 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.libsqlId ||
backup.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
backup: ["delete"],
});
}
const value = await removeBackupById(input.backupId);
if (IS_CLOUD && value) {
removeJob({
@@ -179,6 +256,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 +273,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 +304,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 +331,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 +358,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 +385,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 +410,47 @@ export const backupRouter = createTRPCRouter({
});
}
}),
manualBackupWebServer: protectedProcedure
manualBackupLibsql: protectedProcedure
.input(apiFindOneBackup)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
const backup = await findBackupById(input.backupId);
if (backup.libsqlId) {
await checkServicePermissionAndAccess(ctx, backup.libsqlId, {
backup: ["create"],
});
}
const libsql = await findLibsqlByBackupId(backup.backupId);
await runLibsqlBackup(libsql, backup);
await keepLatestNBackups(backup, libsql?.serverId);
await audit(ctx, {
action: "run",
resourceType: "backup",
resourceId: backup.backupId,
});
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error running manual Libsql backup ",
cause: error,
});
}
}),
manualBackupWebServer: withPermission("backup", "create")
.input(apiFindOneBackup)
.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 +537,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;
@@ -405,6 +573,12 @@ export const backupRouter = createTRPCRouter({
queue.push(log);
});
}
if (input.databaseType === "libsql") {
const libsql = await findLibsqlById(input.databaseId);
restoreLibsqlBackup(libsql, destination, input, (log) => {
queue.push(log);
});
}
if (input.databaseType === "web-server") {
restoreWebServerBackup(destination, input.backupFile, (log) => {
queue.push(log);

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,
@@ -33,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,
@@ -41,7 +45,7 @@ import {
} from "@dokploy/server/templates/github";
import { processTemplate } from "@dokploy/server/templates/processors";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import _ from "lodash";
import { nanoid } from "nanoid";
import { parse } from "toml";
@@ -58,6 +62,8 @@ import {
apiRedeployCompose,
apiUpdateCompose,
compose as composeTable,
environments,
projects,
} from "@/server/db/schema";
import { deploymentWorker } from "@/server/queues/deployments-queue";
import type { DeploymentJob } from "@/server/queues/queue-types";
@@ -69,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({
@@ -76,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({
@@ -105,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;
@@ -122,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 (
@@ -185,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 (
@@ -246,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
@@ -320,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 !== "",
@@ -340,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);
@@ -371,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, {
@@ -424,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",
@@ -449,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(
@@ -459,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",
@@ -468,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",
@@ -491,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(
@@ -501,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",
@@ -510,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
@@ -588,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({
@@ -645,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) {
@@ -678,6 +621,12 @@ export const composeRouter = createTRPCRouter({
}
}
await audit(ctx, {
action: "create",
resourceType: "compose",
resourceId: compose.composeId,
resourceName: compose.name,
});
return compose;
}),
@@ -711,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,
@@ -732,7 +672,6 @@ export const composeRouter = createTRPCRouter({
githubId: null,
triggerType: "push",
// GitLab fields
gitlabRepository: null,
gitlabOwner: null,
gitlabBranch: null,
@@ -740,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;
}),
@@ -775,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)
@@ -815,6 +737,12 @@ export const composeRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "compose",
resourceId: input.composeId,
resourceName: updatedCompose.name,
});
return updatedCompose;
}),
@@ -827,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",
);
@@ -898,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);
}
@@ -990,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",
@@ -1005,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 {
@@ -1034,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",
@@ -1054,4 +974,112 @@ export const composeRouter = createTRPCRouter({
message: "Deployment cancellation only available in cloud version",
});
}),
search: protectedProcedure
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
appName: z.string().optional(),
description: z.string().optional(),
projectId: z.string().optional(),
environmentId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.environmentId) {
baseConditions.push(
eq(composeTable.environmentId, input.environmentId),
);
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(composeTable.name, term),
ilike(composeTable.appName, term),
ilike(composeTable.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(composeTable.name, `%${input.name.trim()}%`));
}
if (input.appName?.trim()) {
baseConditions.push(
ilike(composeTable.appName, `%${input.appName.trim()}%`),
);
}
if (input.description?.trim()) {
baseConditions.push(
ilike(
composeTable.description ?? "",
`%${input.description.trim()}%`,
),
);
}
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);
const [items, countResult] = await Promise.all([
db
.select({
composeId: composeTable.composeId,
name: composeTable.name,
appName: composeTable.appName,
description: composeTable.description,
environmentId: composeTable.environmentId,
composeStatus: composeTable.composeStatus,
sourceType: composeTable.sourceType,
createdAt: composeTable.createdAt,
})
.from(composeTable)
.innerJoin(
environments,
eq(composeTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(composeTable.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(composeTable)
.innerJoin(
environments,
eq(composeTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return {
items,
total: countResult[0]?.count ?? 0,
};
}),
});

View File

@@ -4,74 +4,123 @@ import {
findAllDeploymentsByApplicationId,
findAllDeploymentsByComposeId,
findAllDeploymentsByServerId,
findApplicationById,
findComposeById,
findAllDeploymentsCentralized,
findDeploymentById,
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,
apiFindAllByServer,
apiFindAllByType,
deployments,
server,
} from "@/server/db/schema";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { myQueue } from "@/server/queues/queueSetup";
import { fetchDeployApiJobs, type QueueJobRow } from "@/server/utils/deploy";
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: 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: withPermission("deployment", "read").query(async ({ ctx }) => {
const orgId = ctx.session.activeOrganizationId;
let rows: QueueJobRow[];
if (IS_CLOUD) {
const servers = await db.query.server.findMany({
where: eq(server.organizationId, orgId),
columns: { serverId: true },
});
const serverRowsArrays = await Promise.all(
servers.map(({ serverId }) => fetchDeployApiJobs(serverId)),
);
rows = serverRowsArrays.flat();
rows.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
} else {
const jobs = await myQueue.getJobs();
const jobRows = await Promise.all(
jobs.map(async (job) => {
const state = await job.getState();
return {
id: String(job.id),
name: job.name ?? undefined,
data: job.data as Record<string, unknown>,
timestamp: job.timestamp,
processedOn: job.processedOn,
finishedOn: job.finishedOn,
failedReason: job.failedReason ?? undefined,
state,
};
}),
);
jobRows.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
rows = jobRows;
}
return Promise.all(
rows.map(async (row) => ({
...row,
servicePath: await resolveServicePath(
orgId,
(row.data ?? {}) as Record<string, unknown>,
),
})),
);
}),
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),
@@ -79,18 +128,22 @@ export const deploymentRouter = createTRPCRouter({
rollback: true,
},
});
return deploymentsList;
}),
killProcess: protectedProcedure
.input(
z.object({
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({
@@ -107,6 +160,11 @@ export const deploymentRouter = createTRPCRouter({
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await audit(ctx, {
action: "cancel",
resourceType: "deployment",
resourceId: deployment.deploymentId,
});
}),
removeDeployment: protectedProcedure
@@ -115,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,28 +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";
// Helper function to filter services within an environment based on user permissions
const filterEnvironmentServices = (
environment: any,
accessedServices: string[],
@@ -31,6 +38,12 @@ const filterEnvironmentServices = (
applications: environment.applications.filter((app: any) =>
accessedServices.includes(app.applicationId),
),
compose: environment.compose.filter((comp: any) =>
accessedServices.includes(comp.composeId),
),
libsql: environment.libsql.filter((db: any) =>
accessedServices.includes(db.libsqlId),
),
mariadb: environment.mariadb.filter((db: any) =>
accessedServices.includes(db.mariadbId),
),
@@ -46,9 +59,6 @@ const filterEnvironmentServices = (
redis: environment.redis.filter((db: any) =>
accessedServices.includes(db.redisId),
),
compose: environment.compose.filter((comp: any) =>
accessedServices.includes(comp.composeId),
),
});
export const environmentRouter = createTRPCRouter({
@@ -56,12 +66,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({
@@ -71,16 +76,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) {
@@ -97,54 +101,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
@@ -153,7 +142,6 @@ export const environmentRouter = createTRPCRouter({
try {
const environments = await findEnvironmentsByProjectId(input.projectId);
// Check organization access
if (
environments.some(
(environment) =>
@@ -167,12 +155,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),
@@ -208,7 +197,6 @@ export const environmentRouter = createTRPCRouter({
});
}
// Prevent deletion of the default environment
if (environment.isDefault) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -216,24 +204,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) {
@@ -253,18 +234,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",
@@ -281,9 +258,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,
);
@@ -302,6 +278,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({
@@ -315,14 +299,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 !==
@@ -334,9 +311,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,
);
@@ -350,6 +326,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({
@@ -358,4 +341,92 @@ export const environmentRouter = createTRPCRouter({
});
}
}),
search: protectedProcedure
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
description: z.string().optional(),
projectId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(environments.name, term),
ilike(environments.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(environments.name, `%${input.name.trim()}%`));
}
if (input.description?.trim()) {
baseConditions.push(
ilike(
environments.description ?? "",
`%${input.description.trim()}%`,
),
);
}
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedEnvironments } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedEnvironments.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${environments.environmentId} IN (${sql.join(
accessedEnvironments.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db
.select({
environmentId: environments.environmentId,
name: environments.name,
description: environments.description,
createdAt: environments.createdAt,
env: environments.env,
projectId: environments.projectId,
isDefault: environments.isDefault,
})
.from(environments)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(environments.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(environments)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return {
items,
total: countResult[0]?.count ?? 0,
};
}),
});

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

@@ -0,0 +1,453 @@
import {
checkPortInUse,
createLibsql,
createMount,
deployLibsql,
findEnvironmentById,
findLibsqlById,
findProjectById,
IS_CLOUD,
rebuildDatabase,
removeLibsqlById,
removeService,
startService,
startServiceRemote,
stopService,
stopServiceRemote,
updateLibsqlById,
} from "@dokploy/server";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
} from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import { db } from "@/server/db";
import {
apiChangeLibsqlStatus,
apiCreateLibsql,
apiDeployLibsql,
apiFindOneLibsql,
apiRebuildLibsql,
apiResetLibsql,
apiSaveEnvironmentVariablesLibsql,
apiSaveExternalPortsLibsql,
apiUpdateLibsql,
libsql as libsqlTable,
} from "@/server/db/schema";
export const libsqlRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateLibsql)
.mutation(async ({ input, ctx }) => {
try {
const environment = await findEnvironmentById(input.environmentId);
const project = await findProjectById(environment.projectId);
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a Libsql",
});
}
if (project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this project",
});
}
const newLibsql = await createLibsql({
...input,
});
await addNewService(ctx, newLibsql.libsqlId);
await createMount({
serviceId: newLibsql.libsqlId,
serviceType: "libsql",
volumeName: `${newLibsql.appName}-data`,
mountPath: "/var/lib/sqld",
type: "volume",
});
await audit(ctx, {
action: "create",
resourceType: "service",
resourceId: newLibsql.libsqlId,
resourceName: newLibsql.appName,
});
return true;
} catch (error) {
throw error;
}
}),
one: protectedProcedure
.input(apiFindOneLibsql)
.query(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.libsqlId, "read");
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this Libsql",
});
}
return libsql;
}),
start: protectedProcedure
.input(apiFindOneLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (libsql.serverId) {
await startServiceRemote(libsql.serverId, libsql.appName);
} else {
await startService(libsql.appName);
}
await updateLibsqlById(input.libsqlId, {
applicationStatus: "done",
});
await audit(ctx, {
action: "start",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return libsql;
}),
stop: protectedProcedure
.input(apiFindOneLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (libsql.serverId) {
await stopServiceRemote(libsql.serverId, libsql.appName);
} else {
await stopService(libsql.appName);
}
await updateLibsqlById(input.libsqlId, {
applicationStatus: "idle",
});
await audit(ctx, {
action: "stop",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return libsql;
}),
saveExternalPorts: protectedProcedure
.input(apiSaveExternalPortsLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
service: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (libsql.sqldNode === "replica" && input.externalGRPCPort !== null) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "externalGRPCPort cannot be set when sqldNode is 'replica'",
});
}
const portsToCheck = [
{
port: input.externalPort,
name: "externalPort",
current: libsql.externalPort,
},
{
port: input.externalGRPCPort,
name: "externalGRPCPort",
current: libsql.externalGRPCPort,
},
{
port: input.externalAdminPort,
name: "externalAdminPort",
current: libsql.externalAdminPort,
},
];
for (const { port, name, current } of portsToCheck) {
if (port && port !== current) {
const portCheck = await checkPortInUse(
port,
libsql.serverId || undefined,
);
if (portCheck.isInUse) {
throw new TRPCError({
code: "CONFLICT",
message: `Port ${port} (${name}) is already in use by ${portCheck.conflictingContainer}`,
});
}
}
}
await updateLibsqlById(input.libsqlId, {
externalPort: input.externalPort,
externalGRPCPort: input.externalGRPCPort,
externalAdminPort: input.externalAdminPort,
});
await deployLibsql(input.libsqlId);
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return libsql;
}),
deploy: protectedProcedure
.input(apiDeployLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
await audit(ctx, {
action: "deploy",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return deployLibsql(input.libsqlId);
}),
deployWithLogs: protectedProcedure
.meta({
openapi: {
path: "/deploy/libsql-with-logs",
method: "POST",
override: true,
enabled: false,
},
})
.input(apiDeployLibsql)
.subscription(async function* ({ input, ctx, signal }) {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const queue: string[] = [];
const done = false;
deployLibsql(input.libsqlId, (log) => {
queue.push(log);
});
while (!done || queue.length > 0) {
if (queue.length > 0) {
yield queue.shift()!;
} else {
await new Promise((r) => setTimeout(r, 50));
}
if (signal?.aborted) {
return;
}
}
}),
changeStatus: protectedProcedure
.input(apiChangeLibsqlStatus)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
await updateLibsqlById(input.libsqlId, {
applicationStatus: input.applicationStatus,
});
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return libsql;
}),
remove: protectedProcedure
.input(apiFindOneLibsql)
.mutation(async ({ input, ctx }) => {
await checkServiceAccess(ctx, input.libsqlId, "delete");
const libsql = await findLibsqlById(input.libsqlId);
if (
libsql.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to delete this Libsql",
});
}
await audit(ctx, {
action: "delete",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
const cleanupOperations = [
async () => await removeService(libsql?.appName, libsql.serverId),
async () => await removeLibsqlById(input.libsqlId),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch (_) {}
}
return libsql;
}),
saveEnvironment: protectedProcedure
.input(apiSaveEnvironmentVariablesLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
envVars: ["write"],
});
const service = await updateLibsqlById(input.libsqlId, {
env: input.env,
});
if (!service) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error adding environment variables",
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: input.libsqlId,
});
return true;
}),
reload: protectedProcedure
.input(apiResetLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
const libsql = await findLibsqlById(input.libsqlId);
if (libsql.serverId) {
await stopServiceRemote(libsql.serverId, libsql.appName);
} else {
await stopService(libsql.appName);
}
await updateLibsqlById(input.libsqlId, {
applicationStatus: "idle",
});
if (libsql.serverId) {
await startServiceRemote(libsql.serverId, libsql.appName);
} else {
await startService(libsql.appName);
}
await updateLibsqlById(input.libsqlId, {
applicationStatus: "done",
});
await audit(ctx, {
action: "reload",
resourceType: "service",
resourceId: libsql.libsqlId,
resourceName: libsql.appName,
});
return true;
}),
update: protectedProcedure
.input(apiUpdateLibsql)
.mutation(async ({ input, ctx }) => {
const { libsqlId, ...rest } = input;
await checkServicePermissionAndAccess(ctx, libsqlId, {
service: ["create"],
});
const libsql = await updateLibsqlById(libsqlId, {
...rest,
});
if (!libsql) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error updating Libsql",
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: libsqlId,
resourceName: libsql.appName,
});
return true;
}),
move: protectedProcedure
.input(
z.object({
libsqlId: z.string(),
targetEnvironmentId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
service: ["create"],
});
const updatedLibsql = await db
.update(libsqlTable)
.set({
environmentId: input.targetEnvironmentId,
})
.where(eq(libsqlTable.libsqlId, input.libsqlId))
.returning()
.then((res) => res[0]);
if (!updatedLibsql) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to move libsql",
});
}
await audit(ctx, {
action: "move",
resourceType: "service",
resourceId: updatedLibsql.libsqlId,
resourceName: updatedLibsql.appName,
});
return updatedLibsql;
}),
rebuild: protectedProcedure
.input(apiRebuildLibsql)
.mutation(async ({ input, ctx }) => {
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
deployment: ["create"],
});
await rebuildDatabase(input.libsqlId, "libsql");
await audit(ctx, {
action: "rebuild",
resourceType: "service",
resourceId: input.libsqlId,
});
return true;
}),
});

View File

@@ -1,7 +1,5 @@
import {
addNewService,
checkPortInUse,
checkServiceAccess,
createMariadb,
createMount,
deployMariadb,
@@ -20,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 { eq } from "drizzle-orm";
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,
@@ -35,7 +40,9 @@ import {
apiSaveEnvironmentVariablesMariaDB,
apiSaveExternalPortMariaDB,
apiUpdateMariaDB,
environments,
mariadb as mariadbTable,
projects,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
export const mariadbRouter = createTRPCRouter({
@@ -43,18 +50,10 @@ export const mariadbRouter = createTRPCRouter({
.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({
@@ -72,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,
@@ -88,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) {
@@ -99,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 !==
@@ -123,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 {
@@ -142,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) {
@@ -158,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(
@@ -191,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
@@ -220,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) => {
@@ -240,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 (
@@ -278,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),
@@ -296,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,
});
@@ -317,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 {
@@ -349,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,
});
@@ -376,6 +360,12 @@ export const mariadbRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: mariadbId,
resourceName: service.appName,
});
return true;
}),
move: protectedProcedure
@@ -386,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({
@@ -427,23 +396,124 @@ 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
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
appName: z.string().optional(),
description: z.string().optional(),
projectId: z.string().optional(),
environmentId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.environmentId) {
baseConditions.push(
eq(mariadbTable.environmentId, input.environmentId),
);
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(mariadbTable.name, term),
ilike(mariadbTable.appName, term),
ilike(mariadbTable.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(mariadbTable.name, `%${input.name.trim()}%`));
}
if (input.appName?.trim()) {
baseConditions.push(
ilike(mariadbTable.appName, `%${input.appName.trim()}%`),
);
}
if (input.description?.trim()) {
baseConditions.push(
ilike(
mariadbTable.description ?? "",
`%${input.description.trim()}%`,
),
);
}
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
.select({
mariadbId: mariadbTable.mariadbId,
name: mariadbTable.name,
appName: mariadbTable.appName,
description: mariadbTable.description,
environmentId: mariadbTable.environmentId,
applicationStatus: mariadbTable.applicationStatus,
createdAt: mariadbTable.createdAt,
})
.from(mariadbTable)
.innerJoin(
environments,
eq(mariadbTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(mariadbTable.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(mariadbTable)
.innerJoin(
environments,
eq(mariadbTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return { items, total: countResult[0]?.count ?? 0 };
}),
});

View File

@@ -1,7 +1,5 @@
import {
addNewService,
checkPortInUse,
checkServiceAccess,
createMongo,
createMount,
deployMongo,
@@ -19,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 { eq } from "drizzle-orm";
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,
@@ -36,24 +41,17 @@ import {
apiUpdateMongo,
mongo as mongoTable,
} from "@/server/db/schema";
import { environments, projects } from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
export const mongoRouter = createTRPCRouter({
create: protectedProcedure
.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({
@@ -71,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,
@@ -87,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) {
@@ -102,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 (
@@ -127,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 {
@@ -148,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 {
@@ -174,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(
@@ -207,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
@@ -235,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;
@@ -268,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 {
@@ -313,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);
@@ -338,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 = [
@@ -357,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,
});
@@ -378,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,
});
@@ -405,6 +377,12 @@ export const mongoRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: mongoId,
resourceName: service.appName,
});
return true;
}),
move: protectedProcedure
@@ -415,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({
@@ -456,24 +413,120 @@ 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
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
appName: z.string().optional(),
description: z.string().optional(),
projectId: z.string().optional(),
environmentId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.environmentId) {
baseConditions.push(eq(mongoTable.environmentId, input.environmentId));
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(mongoTable.name, term),
ilike(mongoTable.appName, term),
ilike(mongoTable.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(mongoTable.name, `%${input.name.trim()}%`));
}
if (input.appName?.trim()) {
baseConditions.push(
ilike(mongoTable.appName, `%${input.appName.trim()}%`),
);
}
if (input.description?.trim()) {
baseConditions.push(
ilike(mongoTable.description ?? "", `%${input.description.trim()}%`),
);
}
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
.select({
mongoId: mongoTable.mongoId,
name: mongoTable.name,
appName: mongoTable.appName,
description: mongoTable.description,
environmentId: mongoTable.environmentId,
applicationStatus: mongoTable.applicationStatus,
createdAt: mongoTable.createdAt,
})
.from(mongoTable)
.innerJoin(
environments,
eq(mongoTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(mongoTable.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(mongoTable)
.innerJoin(
environments,
eq(mongoTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return { items, total: countResult[0]?.count ?? 0 };
}),
});

View File

@@ -1,13 +1,12 @@
import {
checkServiceAccess,
createMount,
deleteMount,
findApplicationById,
findComposeById,
findLibsqlById,
findMariadbById,
findMongoById,
findMountById,
findMountOrganizationId,
findMountsByApplicationId,
findMySqlById,
findPostgresById,
@@ -16,8 +15,13 @@ import {
updateMount,
} from "@dokploy/server";
import type { ServiceType } from "@dokploy/server/db/schema/mount";
import {
checkServiceAccess,
checkServicePermissionAndAccess,
} from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateMount,
apiFindMountByApplicationId,
@@ -60,6 +64,10 @@ async function getServiceOrganizationId(
const compose = await findComposeById(serviceId);
return compose?.environment?.project?.organizationId ?? null;
}
case "libsql": {
const libsql = await findLibsqlById(serviceId);
return libsql?.environment?.project?.organizationId ?? null;
}
default:
return null;
}
@@ -68,50 +76,97 @@ async function getServiceOrganizationId(
export const mountRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMount)
.mutation(async ({ input }) => {
await createMount(input);
return true;
.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.libsqlId ||
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.libsqlId ||
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.libsqlId ||
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,15 +177,7 @@ export const mountRouter = createTRPCRouter({
listByServiceId: protectedProcedure
.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,7 +1,5 @@
import {
addNewService,
checkPortInUse,
checkServiceAccess,
createMount,
createMysql,
deployMySql,
@@ -19,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 { eq } from "drizzle-orm";
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,
@@ -34,7 +39,9 @@ import {
apiSaveEnvironmentVariablesMySql,
apiSaveExternalPortMySql,
apiUpdateMySql,
environments,
mysql as mysqlTable,
projects,
} from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
@@ -43,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({
@@ -73,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,
@@ -89,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) {
@@ -104,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 !==
@@ -128,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);
@@ -148,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 {
@@ -172,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(
@@ -205,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
@@ -233,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;
@@ -266,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 {
@@ -310,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 !==
@@ -334,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),
@@ -352,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,
});
@@ -373,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,
});
@@ -400,6 +375,12 @@ export const mysqlRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: mysqlId,
resourceName: service.appName,
});
return true;
}),
move: protectedProcedure
@@ -410,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({
@@ -451,24 +411,120 @@ 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
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
appName: z.string().optional(),
description: z.string().optional(),
projectId: z.string().optional(),
environmentId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.environmentId) {
baseConditions.push(eq(mysqlTable.environmentId, input.environmentId));
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(mysqlTable.name, term),
ilike(mysqlTable.appName, term),
ilike(mysqlTable.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(mysqlTable.name, `%${input.name.trim()}%`));
}
if (input.appName?.trim()) {
baseConditions.push(
ilike(mysqlTable.appName, `%${input.appName.trim()}%`),
);
}
if (input.description?.trim()) {
baseConditions.push(
ilike(mysqlTable.description ?? "", `%${input.description.trim()}%`),
);
}
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
.select({
mysqlId: mysqlTable.mysqlId,
name: mysqlTable.name,
appName: mysqlTable.appName,
description: mysqlTable.description,
environmentId: mysqlTable.environmentId,
applicationStatus: mysqlTable.applicationStatus,
createdAt: mysqlTable.createdAt,
})
.from(mysqlTable)
.innerJoin(
environments,
eq(mysqlTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(mysqlTable.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(mysqlTable)
.innerJoin(
environments,
eq(mysqlTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return { items, total: countResult[0]?.count ?? 0 };
}),
});

View File

@@ -46,11 +46,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,
@@ -94,15 +94,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",
@@ -110,7 +113,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateSlack: adminProcedure
updateSlack: withPermission("notification", "update")
.input(apiUpdateSlack)
.mutation(async ({ input, ctx }) => {
try {
@@ -121,15 +124,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 {
@@ -146,14 +156,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",
@@ -163,7 +178,7 @@ export const notificationRouter = createTRPCRouter({
}
}),
updateTelegram: adminProcedure
updateTelegram: withPermission("notification", "update")
.input(apiUpdateTelegram)
.mutation(async ({ input, ctx }) => {
try {
@@ -174,10 +189,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",
@@ -186,7 +208,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
testTelegramConnection: adminProcedure
testTelegramConnection: withPermission("notification", "create")
.input(apiTestTelegramConnection)
.mutation(async ({ input }) => {
try {
@@ -200,14 +222,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",
@@ -217,7 +244,7 @@ export const notificationRouter = createTRPCRouter({
}
}),
updateDiscord: adminProcedure
updateDiscord: withPermission("notification", "update")
.input(apiUpdateDiscord)
.mutation(async ({ input, ctx }) => {
try {
@@ -228,10 +255,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",
@@ -241,7 +275,7 @@ export const notificationRouter = createTRPCRouter({
}
}),
testDiscordConnection: adminProcedure
testDiscordConnection: withPermission("notification", "create")
.input(apiTestDiscordConnection)
.mutation(async ({ input }) => {
try {
@@ -263,14 +297,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",
@@ -279,7 +315,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateEmail: adminProcedure
updateEmail: withPermission("notification", "update")
.input(apiUpdateEmail)
.mutation(async ({ input, ctx }) => {
try {
@@ -290,10 +326,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",
@@ -302,7 +345,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
testEmailConnection: adminProcedure
testEmailConnection: withPermission("notification", "create")
.input(apiTestEmailConnection)
.mutation(async ({ input }) => {
try {
@@ -320,14 +363,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",
@@ -336,7 +381,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateResend: adminProcedure
updateResend: withPermission("notification", "update")
.input(apiUpdateResend)
.mutation(async ({ input, ctx }) => {
try {
@@ -347,10 +392,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",
@@ -359,7 +411,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
testResendConnection: adminProcedure
testResendConnection: withPermission("notification", "create")
.input(apiTestResendConnection)
.mutation(async ({ input }) => {
try {
@@ -377,7 +429,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
remove: adminProcedure
remove: withPermission("notification", "delete")
.input(apiFindOneNotification)
.mutation(async ({ input, ctx }) => {
try {
@@ -388,6 +440,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 =
@@ -400,7 +457,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
one: protectedProcedure
one: withPermission("notification", "read")
.input(apiFindOneNotification)
.query(async ({ input, ctx }) => {
const notification = await findNotificationById(input.notificationId);
@@ -412,7 +469,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,
@@ -460,8 +517,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 {
@@ -495,14 +550,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",
@@ -511,7 +568,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateGotify: adminProcedure
updateGotify: withPermission("notification", "update")
.input(apiUpdateGotify)
.mutation(async ({ input, ctx }) => {
try {
@@ -525,15 +582,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 {
@@ -551,14 +615,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",
@@ -567,7 +633,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateNtfy: adminProcedure
updateNtfy: withPermission("notification", "update")
.input(apiUpdateNtfy)
.mutation(async ({ input, ctx }) => {
try {
@@ -581,15 +647,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 {
@@ -609,14 +682,19 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
createMattermost: adminProcedure
createMattermost: withPermission("notification", "create")
.input(apiCreateMattermost)
.mutation(async ({ input, ctx }) => {
try {
return await createMattermostNotification(
await createMattermostNotification(
input,
ctx.session.activeOrganizationId,
);
await audit(ctx, {
action: "create",
resourceType: "notification",
resourceName: input.name,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -625,7 +703,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateMattermost: adminProcedure
updateMattermost: withPermission("notification", "update")
.input(apiUpdateMattermost)
.mutation(async ({ input, ctx }) => {
try {
@@ -639,15 +717,22 @@ export const notificationRouter = createTRPCRouter({
message: "You are not authorized to update this notification",
});
}
return await updateMattermostNotification({
const result = await updateMattermostNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "notification",
resourceId: input.notificationId,
resourceName: notification.name,
});
return result;
} catch (error) {
throw error;
}
}),
testMattermostConnection: adminProcedure
testMattermostConnection: withPermission("notification", "create")
.input(apiTestMattermostConnection)
.mutation(async ({ input }) => {
try {
@@ -665,14 +750,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",
@@ -681,7 +768,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateCustom: adminProcedure
updateCustom: withPermission("notification", "update")
.input(apiUpdateCustom)
.mutation(async ({ input, ctx }) => {
try {
@@ -692,15 +779,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 {
@@ -718,14 +812,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",
@@ -734,7 +830,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateLark: adminProcedure
updateLark: withPermission("notification", "update")
.input(apiUpdateLark)
.mutation(async ({ input, ctx }) => {
try {
@@ -748,15 +844,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 {
@@ -775,14 +878,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",
@@ -791,7 +896,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updateTeams: adminProcedure
updateTeams: withPermission("notification", "update")
.input(apiUpdateTeams)
.mutation(async ({ input, ctx }) => {
try {
@@ -805,15 +910,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 {
@@ -830,14 +942,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",
@@ -846,7 +963,7 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
updatePushover: adminProcedure
updatePushover: withPermission("notification", "update")
.input(apiUpdatePushover)
.mutation(async ({ input, ctx }) => {
try {
@@ -860,15 +977,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 {
@@ -886,13 +1010,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,21 @@ 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 }) => {
if (!ctx.session.activeOrganizationId) {
return null;
}
return await db.query.organization.findFirst({
where: eq(organization.id, ctx.session.activeOrganizationId),
});
}),
});

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,7 +1,5 @@
import {
addNewService,
checkPortInUse,
checkServiceAccess,
createMount,
createPostgres,
deployPostgres,
@@ -20,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 { eq } from "drizzle-orm";
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,
@@ -37,6 +42,7 @@ import {
apiUpdatePostgres,
postgres as postgresTable,
} from "@/server/db/schema";
import { environments, projects } from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
export const postgresRouter = createTRPCRouter({
@@ -44,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({
@@ -73,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);
@@ -91,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) {
@@ -106,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 (
@@ -131,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 {
@@ -152,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 {
@@ -176,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,
@@ -210,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);
}),
@@ -239,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;
@@ -274,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 (
@@ -312,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 = [
@@ -331,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,
});
@@ -352,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 {
@@ -384,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,
@@ -412,6 +383,12 @@ export const postgresRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: postgresId,
resourceName: service.appName,
});
return true;
}),
move: protectedProcedure
@@ -422,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({
@@ -463,24 +419,127 @@ 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
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
appName: z.string().optional(),
description: z.string().optional(),
projectId: z.string().optional(),
environmentId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.environmentId) {
baseConditions.push(
eq(postgresTable.environmentId, input.environmentId),
);
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(postgresTable.name, term),
ilike(postgresTable.appName, term),
ilike(postgresTable.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(
ilike(postgresTable.name, `%${input.name.trim()}%`),
);
}
if (input.appName?.trim()) {
baseConditions.push(
ilike(postgresTable.appName, `%${input.appName.trim()}%`),
);
}
if (input.description?.trim()) {
baseConditions.push(
ilike(
postgresTable.description ?? "",
`%${input.description.trim()}%`,
),
);
}
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
.select({
postgresId: postgresTable.postgresId,
name: postgresTable.name,
appName: postgresTable.appName,
description: postgresTable.description,
environmentId: postgresTable.environmentId,
applicationStatus: postgresTable.applicationStatus,
createdAt: postgresTable.createdAt,
})
.from(postgresTable)
.innerJoin(
environments,
eq(postgresTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(postgresTable.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(postgresTable)
.innerJoin(
environments,
eq(postgresTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return { items, total: countResult[0]?.count ?? 0 };
}),
});

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,11 +1,9 @@
import {
addNewEnvironment,
addNewProject,
checkProjectAccess,
createApplication,
createBackup,
createCompose,
createDomain,
createLibsql,
createMariadb,
createMongo,
createMount,
@@ -21,8 +19,8 @@ import {
findApplicationById,
findComposeById,
findEnvironmentById,
findLibsqlById,
findMariadbById,
findMemberById,
findMongoById,
findMySqlById,
findPostgresById,
@@ -33,11 +31,23 @@ import {
updateProjectById,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
addNewEnvironment,
addNewProject,
checkPermission,
checkProjectAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, sql } from "drizzle-orm";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import type { AnyPgColumn } from "drizzle-orm/pg-core";
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 {
apiCreateProject,
apiFindOneProject,
@@ -46,6 +56,7 @@ import {
applications,
compose,
environments,
libsql,
mariadb,
mongo,
mysql,
@@ -59,13 +70,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);
@@ -80,20 +85,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({
@@ -107,18 +108,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(
@@ -140,6 +141,9 @@ export const projectRouter = createTRPCRouter({
accessedServices,
),
},
libsql: {
where: buildServiceFilter(libsql.libsqlId, accessedServices),
},
mariadb: {
where: buildServiceFilter(
mariadb.mariadbId,
@@ -163,6 +167,11 @@ export const projectRouter = createTRPCRouter({
},
},
},
projectTags: {
with: {
tag: true,
},
},
},
});
@@ -185,15 +194,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`
@@ -219,31 +227,82 @@ export const projectRouter = createTRPCRouter({
applications.applicationId,
accessedServices,
),
with: { domains: true },
columns: {
applicationId: true,
name: true,
applicationStatus: true,
},
},
libsql: {
where: buildServiceFilter(libsql.libsqlId, accessedServices),
columns: {
libsqlId: true,
name: true,
applicationStatus: true,
},
},
mariadb: {
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
columns: {
mariadbId: true,
name: true,
applicationStatus: true,
},
},
mongo: {
where: buildServiceFilter(mongo.mongoId, accessedServices),
columns: {
mongoId: true,
name: true,
applicationStatus: true,
},
},
mysql: {
where: buildServiceFilter(mysql.mysqlId, accessedServices),
columns: {
mysqlId: true,
name: true,
applicationStatus: true,
},
},
postgres: {
where: buildServiceFilter(
postgres.postgresId,
accessedServices,
),
columns: {
postgresId: true,
name: true,
applicationStatus: true,
},
},
redis: {
where: buildServiceFilter(redis.redisId, accessedServices),
columns: {
redisId: true,
name: true,
applicationStatus: true,
},
},
compose: {
where: buildServiceFilter(compose.composeId, accessedServices),
with: { domains: true },
columns: {
composeId: true,
name: true,
composeStatus: true,
},
},
},
columns: {
environmentId: true,
isDefault: true,
name: true,
},
},
projectTags: {
with: {
tag: true,
},
},
},
orderBy: desc(projects.createdAt),
@@ -255,20 +314,59 @@ export const projectRouter = createTRPCRouter({
environments: {
with: {
applications: {
with: {
domains: true,
columns: {
applicationId: true,
name: true,
applicationStatus: true,
},
},
mariadb: {
columns: {
mariadbId: true,
},
},
mongo: {
columns: {
mongoId: true,
},
},
mysql: {
columns: {
mysqlId: true,
},
},
postgres: {
columns: {
postgresId: true,
},
},
redis: {
columns: {
redisId: true,
},
},
mariadb: true,
mongo: true,
mysql: true,
postgres: true,
redis: true,
compose: {
with: {
domains: true,
columns: {
composeId: true,
name: true,
composeStatus: true,
},
},
libsql: {
columns: {
libsqlId: true,
},
},
},
columns: {
name: true,
environmentId: true,
isDefault: true,
},
},
projectTags: {
with: {
tag: true,
},
},
},
@@ -277,17 +375,199 @@ export const projectRouter = createTRPCRouter({
});
}),
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,
},
with: {
applications: {
columns: {
applicationId: 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,
},
},
postgres: {
columns: {
postgresId: 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,
},
},
mongo: {
columns: {
mongoId: 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,
},
},
compose: {
columns: {
composeId: true,
appName: true,
name: true,
createdAt: true,
composeStatus: true,
description: true,
serverId: true,
},
},
libsql: {
columns: {
libsqlId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
},
},
},
});
},
),
search: protectedProcedure
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
description: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(projects.name, term),
ilike(projects.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(projects.name, `%${input.name.trim()}%`));
}
if (input.description?.trim()) {
baseConditions.push(
ilike(projects.description ?? "", `%${input.description.trim()}%`),
);
}
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedProjects } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (accessedProjects.length === 0) return { items: [], total: 0 };
baseConditions.push(
sql`${projects.projectId} IN (${sql.join(
accessedProjects.map((id) => sql`${id}`),
sql`, `,
)})`,
);
}
const where = and(...baseConditions);
const [items, countResult] = await Promise.all([
db.query.projects.findMany({
where,
limit: input.limit,
offset: input.offset,
orderBy: desc(projects.createdAt),
columns: {
projectId: true,
name: true,
description: true,
createdAt: true,
organizationId: true,
env: true,
},
}),
db
.select({ count: sql<number>`count(*)::int` })
.from(projects)
.where(where),
]);
return {
items,
total: countResult[0]?.count ?? 0,
};
}),
remove: protectedProcedure
.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
@@ -297,8 +577,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;
@@ -317,10 +604,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;
@@ -339,12 +652,13 @@ export const projectRouter = createTRPCRouter({
id: z.string(),
type: z.enum([
"application",
"postgres",
"compose",
"libsql",
"mariadb",
"mongo",
"mysql",
"postgres",
"redis",
"compose",
]),
}),
)
@@ -354,15 +668,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;
@@ -378,7 +685,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(
@@ -395,7 +719,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": {
@@ -479,21 +802,27 @@ export const projectRouter = createTRPCRouter({
break;
}
case "postgres": {
const { postgresId, mounts, backups, appName, ...postgres } =
await findPostgresById(id);
case "compose": {
const {
composeId,
mounts,
domains,
appName,
refreshToken,
...compose
} = await findComposeById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newPostgres = await createPostgres({
...postgres,
const newCompose = await createCompose({
...compose,
appName: newAppName,
name: input.duplicateInSameProject
? `${postgres.name} (copy)`
: postgres.name,
? `${compose.name} (copy)`
: compose.name,
environmentId: targetProject?.environmentId || "",
});
@@ -501,18 +830,49 @@ export const projectRouter = createTRPCRouter({
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newPostgres.postgresId,
serviceType: "postgres",
serviceId: newCompose.composeId,
serviceType: "compose",
});
}
for (const backup of backups) {
const { backupId, appName: _appName, ...rest } = backup;
await createBackup({
for (const domain of domains) {
const { domainId, ...rest } = domain;
await createDomain({
...rest,
postgresId: newPostgres.postgresId,
composeId: newCompose.composeId,
domainType: "compose",
});
}
break;
}
case "libsql": {
const { libsqlId, mounts, appName, ...libsql } =
await findLibsqlById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newLibsql = await createLibsql({
...libsql,
appName: newAppName,
name: input.duplicateInSameProject
? `${libsql.name} (copy)`
: libsql.name,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newLibsql.libsqlId,
serviceType: "libsql",
});
}
break;
}
case "mariadb": {
@@ -623,6 +983,42 @@ export const projectRouter = createTRPCRouter({
}
break;
}
case "postgres": {
const { postgresId, mounts, backups, appName, ...postgres } =
await findPostgresById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newPostgres = await createPostgres({
...postgres,
appName: newAppName,
name: input.duplicateInSameProject
? `${postgres.name} (copy)`
: postgres.name,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newPostgres.postgresId,
serviceType: "postgres",
});
}
for (const backup of backups) {
const { backupId, ...rest } = backup;
await createBackup({
...rest,
postgresId: newPostgres.postgresId,
});
}
break;
}
case "redis": {
const { redisId, mounts, appName, ...redis } =
await findRedisById(id);
@@ -650,69 +1046,27 @@ export const projectRouter = createTRPCRouter({
});
}
break;
}
case "compose": {
const {
composeId,
mounts,
domains,
appName,
refreshToken,
...compose
} = await findComposeById(id);
const newAppName = appName.substring(
0,
appName.lastIndexOf("-"),
);
const newCompose = await createCompose({
...compose,
appName: newAppName,
name: input.duplicateInSameProject
? `${compose.name} (copy)`
: compose.name,
environmentId: targetProject?.environmentId || "",
});
for (const mount of mounts) {
const { mountId, ...rest } = mount;
await createMount({
...rest,
serviceId: newCompose.composeId,
serviceType: "compose",
});
}
for (const domain of domains) {
const { domainId, ...rest } = domain;
await createDomain({
...rest,
composeId: newCompose.composeId,
domainType: "compose",
});
}
break;
}
}
};
// 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,7 +1,5 @@
import {
addNewService,
checkPortInUse,
checkServiceAccess,
createMount,
createRedis,
deployRedis,
@@ -18,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 { eq } from "drizzle-orm";
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,
@@ -35,23 +40,16 @@ import {
apiUpdateRedis,
redis as redisTable,
} from "@/server/db/schema";
import { environments, projects } from "@/server/db/schema";
export const redisRouter = createTRPCRouter({
create: protectedProcedure
.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({
@@ -69,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,
@@ -85,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;
@@ -93,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 (
@@ -118,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);
@@ -138,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 {
@@ -170,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 {
@@ -195,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(
@@ -228,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
@@ -256,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;
@@ -288,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);
@@ -326,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),
@@ -342,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,
});
@@ -363,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,
});
@@ -380,6 +364,12 @@ export const redisRouter = createTRPCRouter({
});
}
await audit(ctx, {
action: "update",
resourceType: "service",
resourceId: redisId,
resourceName: redis.appName,
});
return true;
}),
move: protectedProcedure
@@ -390,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({
@@ -431,23 +400,119 @@ 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
.input(
z.object({
q: z.string().optional(),
name: z.string().optional(),
appName: z.string().optional(),
description: z.string().optional(),
projectId: z.string().optional(),
environmentId: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
}),
)
.query(async ({ ctx, input }) => {
const baseConditions = [
eq(projects.organizationId, ctx.session.activeOrganizationId),
];
if (input.projectId) {
baseConditions.push(eq(environments.projectId, input.projectId));
}
if (input.environmentId) {
baseConditions.push(eq(redisTable.environmentId, input.environmentId));
}
if (input.q?.trim()) {
const term = `%${input.q.trim()}%`;
baseConditions.push(
or(
ilike(redisTable.name, term),
ilike(redisTable.appName, term),
ilike(redisTable.description ?? "", term),
)!,
);
}
if (input.name?.trim()) {
baseConditions.push(ilike(redisTable.name, `%${input.name.trim()}%`));
}
if (input.appName?.trim()) {
baseConditions.push(
ilike(redisTable.appName, `%${input.appName.trim()}%`),
);
}
if (input.description?.trim()) {
baseConditions.push(
ilike(redisTable.description ?? "", `%${input.description.trim()}%`),
);
}
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
.select({
redisId: redisTable.redisId,
name: redisTable.name,
appName: redisTable.appName,
description: redisTable.description,
environmentId: redisTable.environmentId,
applicationStatus: redisTable.applicationStatus,
createdAt: redisTable.createdAt,
})
.from(redisTable)
.innerJoin(
environments,
eq(redisTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where)
.orderBy(desc(redisTable.createdAt))
.limit(input.limit)
.offset(input.offset),
db
.select({ count: sql<number>`count(*)::int` })
.from(redisTable)
.innerJoin(
environments,
eq(redisTable.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(where),
]);
return { items, total: countResult[0]?.count ?? 0 };
}),
});

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 {
@@ -235,6 +252,8 @@ export const serverRouter = createTRPCRouter({
isDokployNetworkInstalled: boolean;
isSwarmInstalled: boolean;
isMainDirectoryInstalled: boolean;
privilegeMode: string;
dockerGroupMember: boolean;
};
} catch (error) {
throw new TRPCError({
@@ -245,7 +264,7 @@ export const serverRouter = createTRPCRouter({
}
}),
security: protectedProcedure
security: withPermission("server", "read")
.input(apiFindOneServer)
.query(async ({ input, ctx }) => {
try {
@@ -295,7 +314,7 @@ export const serverRouter = createTRPCRouter({
});
}
}),
setupMonitoring: protectedProcedure
setupMonitoring: withPermission("server", "create")
.input(apiUpdateServerMonitoring)
.mutation(async ({ input, ctx }) => {
try {
@@ -332,22 +351,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 +375,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 +395,7 @@ export const serverRouter = createTRPCRouter({
throw error;
}
}),
update: protectedProcedure
update: withPermission("server", "create")
.input(apiUpdateServer)
.mutation(async ({ input, ctx }) => {
try {
@@ -393,6 +417,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 +444,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",
@@ -149,12 +178,12 @@ export const settingsRouter = createTRPCRouter({
// Check if port 8080 is already in use before enabling dashboard
const portCheck = await checkPortInUse(8080, input.serverId);
if (portCheck.isInUse) {
const conflictingContainer = portCheck.conflictingContainer
? ` by container "${portCheck.conflictingContainer}"`
const conflictInfo = portCheck.conflictingContainer
? ` by ${portCheck.conflictingContainer}`
: "";
throw new TRPCError({
code: "CONFLICT",
message: `Port 8080 is already in use${conflictingContainer}. Please stop the conflicting service or use a different port for the Traefik dashboard.`,
message: `Port 8080 is already in use${conflictInfo}. Please stop the conflicting service or use a different port for the Traefik dashboard.`,
});
}
newPorts.push({
@@ -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;
}),
@@ -559,6 +648,7 @@ export const settingsRouter = createTRPCRouter({
"postgres",
"redis",
"mongo",
"libsql",
"mariadb",
"sshRouter",
"gitProvider",
@@ -567,6 +657,18 @@ export const settingsRouter = createTRPCRouter({
"github",
"gitlab",
"gitea",
"tag",
"patch",
"server",
"volumeBackups",
"environment",
"auditLog",
"customRole",
"whitelabeling",
"sso",
"licenseKey",
"organization",
"previewDeployment",
],
});
@@ -610,7 +712,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 +724,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 +822,7 @@ export const settingsRouter = createTRPCRouter({
enable: z.boolean(),
}),
)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
return true;
}
@@ -742,7 +849,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 +880,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 +968,7 @@ export const settingsRouter = createTRPCRouter({
),
}),
)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
try {
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
@@ -873,6 +1006,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 +1035,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

@@ -0,0 +1,439 @@
import { findMemberByUserId } from "@dokploy/server/services/permission";
import { TRPCError } from "@trpc/server";
import { and, eq } from "drizzle-orm";
import { z } from "zod";
import { db } from "@/server/db";
import {
apiCreateTag,
apiFindOneTag,
apiRemoveTag,
apiUpdateTag,
projects,
projectTags,
tags,
} from "@/server/db/schema";
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const tagRouter = createTRPCRouter({
create: withPermission("tag", "create")
.input(apiCreateTag)
.mutation(async ({ input, ctx }) => {
try {
const newTag = await db
.insert(tags)
.values({
name: input.name,
color: input.color,
organizationId: ctx.session.activeOrganizationId,
})
.returning();
return newTag[0];
} catch (error) {
if (
error instanceof Error &&
error.message.includes("unique_org_tag_name")
) {
throw new TRPCError({
code: "CONFLICT",
message: "A tag with this name already exists in your organization",
});
}
throw new TRPCError({
code: "BAD_REQUEST",
message: `Error creating tag: ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
}),
all: protectedProcedure.query(async ({ ctx }) => {
try {
const organizationTags = await db.query.tags.findMany({
where: eq(tags.organizationId, ctx.session.activeOrganizationId),
orderBy: (tags, { asc }) => [asc(tags.name)],
});
return organizationTags;
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error fetching tags: ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
}),
one: protectedProcedure.input(apiFindOneTag).query(async ({ input, ctx }) => {
try {
const tag = await db.query.tags.findFirst({
where: and(
eq(tags.tagId, input.tagId),
eq(tags.organizationId, ctx.session.activeOrganizationId),
),
});
if (!tag) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Tag not found",
});
}
return tag;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error fetching tag: ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
}),
update: withPermission("tag", "update")
.input(apiUpdateTag)
.mutation(async ({ input, ctx }) => {
try {
// First verify the tag belongs to the user's organization
const existingTag = await db.query.tags.findFirst({
where: and(
eq(tags.tagId, input.tagId),
eq(tags.organizationId, ctx.session.activeOrganizationId),
),
});
if (!existingTag) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Tag not found or you don't have permission to update it",
});
}
const updatedTag = await db
.update(tags)
.set({
...(input.name !== undefined && { name: input.name }),
...(input.color !== undefined && { color: input.color }),
})
.where(eq(tags.tagId, input.tagId))
.returning();
return updatedTag[0];
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
if (
error instanceof Error &&
error.message.includes("unique_org_tag_name")
) {
throw new TRPCError({
code: "CONFLICT",
message: "A tag with this name already exists in your organization",
});
}
throw new TRPCError({
code: "BAD_REQUEST",
message: `Error updating tag: ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
}),
remove: withPermission("tag", "delete")
.input(apiRemoveTag)
.mutation(async ({ input, ctx }) => {
try {
// First verify the tag belongs to the user's organization
const existingTag = await db.query.tags.findFirst({
where: and(
eq(tags.tagId, input.tagId),
eq(tags.organizationId, ctx.session.activeOrganizationId),
),
});
if (!existingTag) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Tag not found or you don't have permission to delete it",
});
}
// Delete the tag - cascade delete will handle projectTags associations
await db.delete(tags).where(eq(tags.tagId, input.tagId));
return { success: true };
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: `Error deleting tag: ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
}),
assignToProject: protectedProcedure
.input(
z.object({
projectId: z.string().min(1),
tagId: z.string().min(1),
}),
)
.mutation(async ({ input, ctx }) => {
try {
const memberRecord = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
// Verify the project belongs to the user's organization
const project = await db.query.projects.findFirst({
where: and(
eq(projects.projectId, input.projectId),
eq(projects.organizationId, ctx.session.activeOrganizationId),
),
});
if (!project) {
throw new TRPCError({
code: "NOT_FOUND",
message:
"Project not found or you don't have permission to modify it",
});
}
// Verify the member has access to the project
if (
memberRecord.role !== "owner" &&
memberRecord.role !== "admin" &&
!memberRecord.accessedProjects.includes(input.projectId)
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}
// Verify the tag belongs to the user's organization
const tag = await db.query.tags.findFirst({
where: and(
eq(tags.tagId, input.tagId),
eq(tags.organizationId, ctx.session.activeOrganizationId),
),
});
if (!tag) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Tag not found or you don't have permission to use it",
});
}
// Insert the project-tag association
const newAssociation = await db
.insert(projectTags)
.values({
projectId: input.projectId,
tagId: input.tagId,
})
.returning();
return newAssociation[0];
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
if (
error instanceof Error &&
error.message.includes("unique_project_tag")
) {
throw new TRPCError({
code: "CONFLICT",
message: "This tag is already assigned to this project",
});
}
throw new TRPCError({
code: "BAD_REQUEST",
message: `Error assigning tag to project: ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
}),
removeFromProject: protectedProcedure
.input(
z.object({
projectId: z.string().min(1),
tagId: z.string().min(1),
}),
)
.mutation(async ({ input, ctx }) => {
try {
const memberRecord = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
// Verify the project belongs to the user's organization
const project = await db.query.projects.findFirst({
where: and(
eq(projects.projectId, input.projectId),
eq(projects.organizationId, ctx.session.activeOrganizationId),
),
});
if (!project) {
throw new TRPCError({
code: "NOT_FOUND",
message:
"Project not found or you don't have permission to modify it",
});
}
// Verify the member has access to the project
if (
memberRecord.role !== "owner" &&
memberRecord.role !== "admin" &&
!memberRecord.accessedProjects.includes(input.projectId)
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}
// Verify the tag belongs to the user's organization
const tag = await db.query.tags.findFirst({
where: and(
eq(tags.tagId, input.tagId),
eq(tags.organizationId, ctx.session.activeOrganizationId),
),
});
if (!tag) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Tag not found or you don't have permission to use it",
});
}
// Delete the project-tag association
await db
.delete(projectTags)
.where(
and(
eq(projectTags.projectId, input.projectId),
eq(projectTags.tagId, input.tagId),
),
);
return { success: true };
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: `Error removing tag from project: ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
}),
bulkAssign: protectedProcedure
.input(
z.object({
projectId: z.string().min(1),
tagIds: z.array(z.string().min(1)),
}),
)
.mutation(async ({ input, ctx }) => {
try {
const memberRecord = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
// Verify the project belongs to the user's organization
const project = await db.query.projects.findFirst({
where: and(
eq(projects.projectId, input.projectId),
eq(projects.organizationId, ctx.session.activeOrganizationId),
),
});
if (!project) {
throw new TRPCError({
code: "NOT_FOUND",
message:
"Project not found or you don't have permission to modify it",
});
}
// Verify the member has access to the project
if (
memberRecord.role !== "owner" &&
memberRecord.role !== "admin" &&
!memberRecord.accessedProjects.includes(input.projectId)
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}
// Verify all tags belong to the user's organization
if (input.tagIds.length > 0) {
const tagCount = await db.query.tags.findMany({
where: and(
eq(tags.organizationId, ctx.session.activeOrganizationId),
),
});
const validTagIds = tagCount.map((tag) => tag.tagId);
const invalidTags = input.tagIds.filter(
(id) => !validTagIds.includes(id),
);
if (invalidTags.length > 0) {
throw new TRPCError({
code: "NOT_FOUND",
message: "One or more tags not found in your organization",
});
}
}
// Delete all existing tag associations for this project
await db
.delete(projectTags)
.where(eq(projectTags.projectId, input.projectId));
// Insert new tag associations
if (input.tagIds.length > 0) {
await db.insert(projectTags).values(
input.tagIds.map((tagId) => ({
projectId: input.projectId,
tagId,
})),
);
}
return { success: true };
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: `Error bulk assigning tags to project: ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
}),
});

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,20 +93,37 @@ 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: publicProcedure.query(async ({ ctx }) => {
if (!ctx.user || !ctx.session || !ctx.session.activeOrganizationId) {
return null;
}
return {
user: {
id: ctx.user.id,
},
session: {
activeOrganizationId: ctx.session.activeOrganizationId,
},
};
}),
get: protectedProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(
@@ -118,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;
@@ -153,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 }) => {
@@ -200,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",
@@ -214,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({
@@ -284,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 {
@@ -317,6 +360,12 @@ export const userRouter = createTRPCRouter({
),
),
);
await audit(ctx, {
action: "update",
resourceType: "user",
resourceId: input.id,
metadata: { permissions: rest },
});
} catch (error) {
throw error;
}
@@ -334,7 +383,7 @@ export const userRouter = createTRPCRouter({
});
}),
getContainerMetrics: protectedProcedure
getContainerMetrics: withPermission("monitoring", "read")
.input(
z.object({
url: z.string(),
@@ -416,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",
@@ -424,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;
@@ -451,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;
}),
@@ -495,7 +556,7 @@ export const userRouter = createTRPCRouter({
return organizations.length;
}),
sendInvitation: adminProcedure
sendInvitation: withPermission("member", "create")
.input(
z.object({
invitationId: z.string().min(1),
@@ -561,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

@@ -15,6 +15,7 @@ import {
updateVolumeBackupSchema,
volumeBackups,
} from "@dokploy/server/db/schema";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import {
execAsyncRemote,
execAsyncStream,
@@ -23,8 +24,9 @@ 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 { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
export const volumeBackupsRouter = createTRPCRouter({
list: protectedProcedure
@@ -39,10 +41,14 @@ export const volumeBackupsRouter = createTRPCRouter({
"mongo",
"redis",
"compose",
"libsql",
]),
}),
)
.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: {
@@ -53,13 +59,28 @@ export const volumeBackupsRouter = createTRPCRouter({
mongo: true,
redis: true,
compose: true,
libsql: true,
},
orderBy: [desc(volumeBackups.createdAt)],
});
}),
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.libsqlId ||
input.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volumeBackup: ["create"],
});
}
const newVolumeBackup = await createVolumeBackup(input);
if (newVolumeBackup?.enabled) {
@@ -73,6 +94,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 +107,23 @@ 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.libsqlId ||
vb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volumeBackup: ["read"],
});
}
return vb;
}),
delete: protectedProcedure
.input(
@@ -90,12 +131,48 @@ 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.libsqlId ||
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.libsqlId ||
existingVb.composeId;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volumeBackup: ["update"],
});
}
const updatedVolumeBackup = await updateVolumeBackup(
input.volumeBackupId,
input,
@@ -130,20 +207,46 @@ 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.libsqlId ||
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,
});