mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-06 06:25:28 +02:00
Merge branch 'canary' into feat/add-mattermost-notification-provider
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
453
apps/dokploy/server/api/routers/libsql.ts
Normal file
453
apps/dokploy/server/api/routers/libsql.ts
Normal 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;
|
||||
}),
|
||||
});
|
||||
@@ -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 };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
67
apps/dokploy/server/api/routers/proprietary/audit-log.ts
Normal file
67
apps/dokploy/server/api/routers/proprietary/audit-log.ts
Normal 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,
|
||||
});
|
||||
}),
|
||||
});
|
||||
321
apps/dokploy/server/api/routers/proprietary/custom-role.ts
Normal file
321
apps/dokploy/server/api/routers/proprietary/custom-role.ts
Normal 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(", ")}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
106
apps/dokploy/server/api/routers/proprietary/whitelabeling.ts
Normal file
106
apps/dokploy/server/api/routers/proprietary/whitelabeling.ts
Normal 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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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 ?? ""),
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
});
|
||||
|
||||
439
apps/dokploy/server/api/routers/tag.ts
Normal file
439
apps/dokploy/server/api/routers/tag.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
31
apps/dokploy/server/api/utils/audit.ts
Normal file
31
apps/dokploy/server/api/utils/audit.ts
Normal 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,
|
||||
});
|
||||
Reference in New Issue
Block a user