mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-27 10:05:32 +02:00
Merge branch 'canary' into feat/quick-service-switcher
This commit is contained in:
@@ -27,8 +27,11 @@ import { portRouter } from "./routers/port";
|
||||
import { postgresRouter } from "./routers/postgres";
|
||||
import { previewDeploymentRouter } from "./routers/preview-deployment";
|
||||
import { projectRouter } from "./routers/project";
|
||||
import { auditLogRouter } from "./routers/proprietary/audit-log";
|
||||
import { customRoleRouter } from "./routers/proprietary/custom-role";
|
||||
import { licenseKeyRouter } from "./routers/proprietary/license-key";
|
||||
import { ssoRouter } from "./routers/proprietary/sso";
|
||||
import { whitelabelingRouter } from "./routers/proprietary/whitelabeling";
|
||||
import { redirectsRouter } from "./routers/redirects";
|
||||
import { redisRouter } from "./routers/redis";
|
||||
import { registryRouter } from "./routers/registry";
|
||||
@@ -87,6 +90,9 @@ export const appRouter = createTRPCRouter({
|
||||
organization: organizationRouter,
|
||||
licenseKey: licenseKeyRouter,
|
||||
sso: ssoRouter,
|
||||
whitelabeling: whitelabelingRouter,
|
||||
customRole: customRoleRouter,
|
||||
auditLog: auditLogRouter,
|
||||
schedule: scheduleRouter,
|
||||
rollback: rollbackRouter,
|
||||
volumeBackups: volumeBackupsRouter,
|
||||
|
||||
@@ -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
@@ -44,7 +44,13 @@ import {
|
||||
} from "@dokploy/server/utils/restore";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateBackup,
|
||||
apiFindOneBackup,
|
||||
@@ -69,10 +75,21 @@ interface RcloneFile {
|
||||
export const backupRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const newBackup = await createBackup(input);
|
||||
const serviceId =
|
||||
input.postgresId ||
|
||||
input.mysqlId ||
|
||||
input.mariadbId ||
|
||||
input.mongoId ||
|
||||
input.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
backup: ["create"],
|
||||
});
|
||||
}
|
||||
|
||||
const newBackup = await createBackup(input);
|
||||
const backup = await findBackupById(newBackup.backupId);
|
||||
|
||||
if (IS_CLOUD && backup.enabled) {
|
||||
@@ -110,6 +127,11 @@ export const backupRouter = createTRPCRouter({
|
||||
scheduleBackup(backup);
|
||||
}
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new TRPCError({
|
||||
@@ -122,15 +144,42 @@ export const backupRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindOneBackup).query(async ({ input }) => {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
|
||||
return backup;
|
||||
}),
|
||||
const serviceId =
|
||||
backup.postgresId ||
|
||||
backup.mysqlId ||
|
||||
backup.mariadbId ||
|
||||
backup.mongoId ||
|
||||
backup.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
backup: ["read"],
|
||||
});
|
||||
}
|
||||
|
||||
return backup;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const existing = await findBackupById(input.backupId);
|
||||
const serviceId =
|
||||
existing.postgresId ||
|
||||
existing.mysqlId ||
|
||||
existing.mariadbId ||
|
||||
existing.mongoId ||
|
||||
existing.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
backup: ["update"],
|
||||
});
|
||||
}
|
||||
|
||||
await updateBackupById(input.backupId, input);
|
||||
const backup = await findBackupById(input.backupId);
|
||||
|
||||
@@ -156,6 +205,11 @@ export const backupRouter = createTRPCRouter({
|
||||
removeScheduleBackup(input.backupId);
|
||||
}
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Error updating this Backup";
|
||||
@@ -167,8 +221,21 @@ export const backupRouter = createTRPCRouter({
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiRemoveBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
const serviceId =
|
||||
backup.postgresId ||
|
||||
backup.mysqlId ||
|
||||
backup.mariadbId ||
|
||||
backup.mongoId ||
|
||||
backup.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
backup: ["delete"],
|
||||
});
|
||||
}
|
||||
|
||||
const value = await removeBackupById(input.backupId);
|
||||
if (IS_CLOUD && value) {
|
||||
removeJob({
|
||||
@@ -179,6 +246,11 @@ export const backupRouter = createTRPCRouter({
|
||||
} else if (!IS_CLOUD) {
|
||||
removeScheduleBackup(input.backupId);
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "backup",
|
||||
resourceId: input.backupId,
|
||||
});
|
||||
return value;
|
||||
} catch (error) {
|
||||
const message =
|
||||
@@ -191,13 +263,22 @@ export const backupRouter = createTRPCRouter({
|
||||
}),
|
||||
manualBackupPostgres: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
if (backup.postgresId) {
|
||||
await checkServicePermissionAndAccess(ctx, backup.postgresId, {
|
||||
backup: ["create"],
|
||||
});
|
||||
}
|
||||
const postgres = await findPostgresByBackupId(backup.backupId);
|
||||
await runPostgresBackup(postgres, backup);
|
||||
|
||||
await keepLatestNBackups(backup, postgres?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const message =
|
||||
@@ -213,12 +294,22 @@ export const backupRouter = createTRPCRouter({
|
||||
|
||||
manualBackupMySql: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
if (backup.mysqlId) {
|
||||
await checkServicePermissionAndAccess(ctx, backup.mysqlId, {
|
||||
backup: ["create"],
|
||||
});
|
||||
}
|
||||
const mysql = await findMySqlByBackupId(backup.backupId);
|
||||
await runMySqlBackup(mysql, backup);
|
||||
await keepLatestNBackups(backup, mysql?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -230,12 +321,22 @@ export const backupRouter = createTRPCRouter({
|
||||
}),
|
||||
manualBackupMariadb: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
if (backup.mariadbId) {
|
||||
await checkServicePermissionAndAccess(ctx, backup.mariadbId, {
|
||||
backup: ["create"],
|
||||
});
|
||||
}
|
||||
const mariadb = await findMariadbByBackupId(backup.backupId);
|
||||
await runMariadbBackup(mariadb, backup);
|
||||
await keepLatestNBackups(backup, mariadb?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -247,12 +348,22 @@ export const backupRouter = createTRPCRouter({
|
||||
}),
|
||||
manualBackupCompose: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
if (backup.composeId) {
|
||||
await checkServicePermissionAndAccess(ctx, backup.composeId, {
|
||||
backup: ["create"],
|
||||
});
|
||||
}
|
||||
const compose = await findComposeByBackupId(backup.backupId);
|
||||
await runComposeBackup(compose, backup);
|
||||
await keepLatestNBackups(backup, compose?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -264,12 +375,22 @@ export const backupRouter = createTRPCRouter({
|
||||
}),
|
||||
manualBackupMongo: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
if (backup.mongoId) {
|
||||
await checkServicePermissionAndAccess(ctx, backup.mongoId, {
|
||||
backup: ["create"],
|
||||
});
|
||||
}
|
||||
const mongo = await findMongoByBackupId(backup.backupId);
|
||||
await runMongoBackup(mongo, backup);
|
||||
await keepLatestNBackups(backup, mongo?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -279,15 +400,20 @@ export const backupRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
manualBackupWebServer: protectedProcedure
|
||||
manualBackupWebServer: withPermission("backup", "create")
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
await runWebServerBackup(backup);
|
||||
await keepLatestNBackups(backup);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
listBackupFiles: protectedProcedure
|
||||
listBackupFiles: withPermission("backup", "read")
|
||||
.input(
|
||||
z.object({
|
||||
destinationId: z.string(),
|
||||
@@ -374,7 +500,12 @@ export const backupRouter = createTRPCRouter({
|
||||
},
|
||||
})
|
||||
.input(apiRestoreBackup)
|
||||
.subscription(async function* ({ input, signal }) {
|
||||
.subscription(async function* ({ input, ctx, signal }) {
|
||||
if (input.databaseId) {
|
||||
await checkServicePermissionAndAccess(ctx, input.databaseId, {
|
||||
backup: ["restore"],
|
||||
});
|
||||
}
|
||||
const destination = await findDestinationById(input.destinationId);
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
|
||||
@@ -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,
|
||||
@@ -16,7 +14,6 @@ import {
|
||||
findDomainsByComposeId,
|
||||
findEnvironmentById,
|
||||
findGitProviderById,
|
||||
findMemberById,
|
||||
findProjectById,
|
||||
findServerById,
|
||||
getComposeContainer,
|
||||
@@ -34,6 +31,12 @@ import {
|
||||
updateCompose,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type CompleteTemplate,
|
||||
@@ -72,6 +75,7 @@ import {
|
||||
} from "@/server/queues/queueSetup";
|
||||
import { cancelDeployment, deploy } from "@/server/utils/deploy";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
import { audit } from "../utils/audit";
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
|
||||
export const composeRouter = createTRPCRouter({
|
||||
@@ -79,18 +83,10 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiCreateCompose)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, project.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -108,14 +104,14 @@ export const composeRouter = createTRPCRouter({
|
||||
...input,
|
||||
});
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newService.composeId,
|
||||
project.organizationId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, newService.composeId);
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "service",
|
||||
resourceId: newService.composeId,
|
||||
resourceName: newService.appName,
|
||||
});
|
||||
return newService;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -125,14 +121,7 @@ export const composeRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.composeId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.composeId, "read");
|
||||
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
@@ -188,29 +177,22 @@ export const composeRouter = createTRPCRouter({
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this compose",
|
||||
});
|
||||
}
|
||||
return updateCompose(input.composeId, input);
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const updated = await updateCompose(input.composeId, input);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: updated?.name,
|
||||
});
|
||||
return updated;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(apiDeleteCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.composeId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"delete",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.composeId, "delete");
|
||||
const composeResult = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
@@ -249,70 +231,55 @@ export const composeRouter = createTRPCRouter({
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "service",
|
||||
resourceId: composeResult.composeId,
|
||||
resourceName: composeResult.appName,
|
||||
});
|
||||
return composeResult;
|
||||
}),
|
||||
cleanQueues: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to clean this compose",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
await cleanQueuesByCompose(input.composeId);
|
||||
return { success: true, message: "Queues cleaned successfully" };
|
||||
}),
|
||||
clearDeployments: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message:
|
||||
"You are not authorized to clear deployments for this compose",
|
||||
});
|
||||
}
|
||||
await clearOldDeployments(compose.appName, compose.serverId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
killBuild: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["cancel"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to kill this build",
|
||||
});
|
||||
}
|
||||
await killDockerBuild("compose", compose.serverId);
|
||||
}),
|
||||
|
||||
loadServices: protectedProcedure
|
||||
.input(apiFetchServices)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to load this compose",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
return await loadServices(input.composeId, input.type);
|
||||
}),
|
||||
loadMountsByService: protectedProcedure
|
||||
@@ -323,16 +290,10 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to load this compose",
|
||||
});
|
||||
}
|
||||
const container = await getComposeContainer(compose, input.serviceName);
|
||||
const mounts = container?.Mounts.filter(
|
||||
(mount) => mount.Type === "volume" && mount.Source !== "",
|
||||
@@ -343,18 +304,11 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to fetch this compose",
|
||||
});
|
||||
}
|
||||
|
||||
const command = await cloneCompose(compose);
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
@@ -374,49 +328,45 @@ export const composeRouter = createTRPCRouter({
|
||||
randomizeCompose: protectedProcedure
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const result = await randomizeComposeFile(input.composeId, input.suffix);
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to randomize this compose",
|
||||
});
|
||||
}
|
||||
return await randomizeComposeFile(input.composeId, input.suffix);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
isolatedDeployment: protectedProcedure
|
||||
.input(apiRandomizeCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to randomize this compose",
|
||||
});
|
||||
}
|
||||
return await randomizeIsolatedDeploymentComposeFile(
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const result = await randomizeIsolatedDeploymentComposeFile(
|
||||
input.composeId,
|
||||
input.suffix,
|
||||
);
|
||||
const compose = await findComposeById(input.composeId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
getConvertedCompose: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to get this compose",
|
||||
});
|
||||
}
|
||||
const domains = await findDomainsByComposeId(input.composeId);
|
||||
const composeFile = await addDomainToCompose(compose, domains);
|
||||
return stringify(composeFile, {
|
||||
@@ -427,17 +377,11 @@ export const composeRouter = createTRPCRouter({
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this compose",
|
||||
});
|
||||
}
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: input.composeId,
|
||||
titleLog: input.title || "Manual deployment",
|
||||
@@ -452,6 +396,12 @@ export const composeRouter = createTRPCRouter({
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
@@ -462,6 +412,12 @@ export const composeRouter = createTRPCRouter({
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Deployment queued",
|
||||
@@ -471,16 +427,10 @@ export const composeRouter = createTRPCRouter({
|
||||
redeploy: protectedProcedure
|
||||
.input(apiRedeployCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to redeploy this compose",
|
||||
});
|
||||
}
|
||||
const jobData: DeploymentJob = {
|
||||
composeId: input.composeId,
|
||||
titleLog: input.title || "Rebuild deployment",
|
||||
@@ -494,6 +444,12 @@ export const composeRouter = createTRPCRouter({
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
@@ -504,6 +460,12 @@ export const composeRouter = createTRPCRouter({
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Redeployment queued",
|
||||
@@ -513,70 +475,61 @@ export const composeRouter = createTRPCRouter({
|
||||
stop: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this compose",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
await stopCompose(input.composeId);
|
||||
|
||||
const composeForStop = await findComposeById(input.composeId);
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: composeForStop.name,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
start: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this compose",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
await startCompose(input.composeId);
|
||||
|
||||
const composeForStart = await findComposeById(input.composeId);
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: composeForStart.name,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
getDefaultCommand: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to get this compose",
|
||||
});
|
||||
}
|
||||
const command = createCommand(compose);
|
||||
return `docker ${command}`;
|
||||
}),
|
||||
refreshToken: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to refresh this compose",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
await updateCompose(input.composeId, {
|
||||
refreshToken: nanoid(),
|
||||
});
|
||||
const composeForToken = await findComposeById(input.composeId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: composeForToken.name,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
deployTemplate: protectedProcedure
|
||||
@@ -591,14 +544,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
environment.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, environment.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -648,13 +594,7 @@ export const composeRouter = createTRPCRouter({
|
||||
isolatedDeployment: true,
|
||||
});
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
compose.composeId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, compose.composeId);
|
||||
|
||||
if (generate.mounts && generate.mounts?.length > 0) {
|
||||
for (const mount of generate.mounts) {
|
||||
@@ -681,6 +621,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "compose",
|
||||
resourceId: compose.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return compose;
|
||||
}),
|
||||
|
||||
@@ -714,20 +660,11 @@ export const composeRouter = createTRPCRouter({
|
||||
disconnectGitProvider: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to disconnect this git provider",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
// Reset all git provider related fields
|
||||
await updateCompose(input.composeId, {
|
||||
// GitHub fields
|
||||
repository: null,
|
||||
branch: null,
|
||||
owner: null,
|
||||
@@ -735,7 +672,6 @@ export const composeRouter = createTRPCRouter({
|
||||
githubId: null,
|
||||
triggerType: "push",
|
||||
|
||||
// GitLab fields
|
||||
gitlabRepository: null,
|
||||
gitlabOwner: null,
|
||||
gitlabBranch: null,
|
||||
@@ -743,30 +679,33 @@ export const composeRouter = createTRPCRouter({
|
||||
gitlabProjectId: null,
|
||||
gitlabPathNamespace: null,
|
||||
|
||||
// Bitbucket fields
|
||||
bitbucketRepository: null,
|
||||
bitbucketOwner: null,
|
||||
bitbucketBranch: null,
|
||||
bitbucketId: null,
|
||||
|
||||
// Gitea fields
|
||||
giteaRepository: null,
|
||||
giteaOwner: null,
|
||||
giteaBranch: null,
|
||||
giteaId: null,
|
||||
|
||||
// Custom Git fields
|
||||
customGitBranch: null,
|
||||
customGitUrl: null,
|
||||
customGitSSHKeyId: null,
|
||||
|
||||
// Common fields
|
||||
sourceType: "github", // Reset to default
|
||||
composeStatus: "idle",
|
||||
watchPaths: null,
|
||||
enableSubmodules: false,
|
||||
});
|
||||
|
||||
const composeForDisconnect = await findComposeById(input.composeId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: composeForDisconnect.name,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
|
||||
@@ -778,29 +717,9 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this compose",
|
||||
});
|
||||
}
|
||||
|
||||
const targetEnvironment = await findEnvironmentById(
|
||||
input.targetEnvironmentId,
|
||||
);
|
||||
if (
|
||||
targetEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this environment",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
const updatedCompose = await db
|
||||
.update(composeTable)
|
||||
@@ -818,6 +737,12 @@ export const composeRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: updatedCompose.name,
|
||||
});
|
||||
return updatedCompose;
|
||||
}),
|
||||
|
||||
@@ -830,18 +755,11 @@ export const composeRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this compose",
|
||||
});
|
||||
}
|
||||
|
||||
const decodedData = Buffer.from(input.base64, "base64").toString(
|
||||
"utf-8",
|
||||
);
|
||||
@@ -901,21 +819,14 @@ export const composeRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
const decodedData = Buffer.from(input.base64, "base64").toString(
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this compose",
|
||||
});
|
||||
}
|
||||
|
||||
for (const mount of compose.mounts) {
|
||||
await deleteMount(mount.mountId);
|
||||
}
|
||||
@@ -993,6 +904,12 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.appName,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Template imported successfully",
|
||||
@@ -1008,16 +925,10 @@ export const composeRouter = createTRPCRouter({
|
||||
cancelDeployment: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["cancel"],
|
||||
});
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to cancel this deployment",
|
||||
});
|
||||
}
|
||||
|
||||
if (IS_CLOUD && compose.serverId) {
|
||||
try {
|
||||
@@ -1037,6 +948,12 @@ export const composeRouter = createTRPCRouter({
|
||||
applicationType: "compose",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "compose",
|
||||
resourceId: input.composeId,
|
||||
resourceName: compose.name,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: "Deployment cancellation requested",
|
||||
@@ -1113,19 +1030,17 @@ export const composeRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${composeTable.composeId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const { accessedServices } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${composeTable.composeId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
|
||||
const where = and(...baseConditions);
|
||||
|
||||
|
||||
@@ -5,20 +5,21 @@ import {
|
||||
findAllDeploymentsByComposeId,
|
||||
findAllDeploymentsByServerId,
|
||||
findAllDeploymentsCentralized,
|
||||
findApplicationById,
|
||||
findComposeById,
|
||||
findDeploymentById,
|
||||
findMemberById,
|
||||
findServerById,
|
||||
IS_CLOUD,
|
||||
removeDeployment,
|
||||
resolveServicePath,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiFindAllByApplication,
|
||||
apiFindAllByCompose,
|
||||
@@ -29,65 +30,46 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { fetchDeployApiJobs, type QueueJobRow } from "@/server/utils/deploy";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
|
||||
export const deploymentRouter = createTRPCRouter({
|
||||
all: protectedProcedure
|
||||
.input(apiFindAllByApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this application",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
deployment: ["read"],
|
||||
});
|
||||
return await findAllDeploymentsByApplicationId(input.applicationId);
|
||||
}),
|
||||
|
||||
allByCompose: protectedProcedure
|
||||
.input(apiFindAllByCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this compose",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.composeId, {
|
||||
deployment: ["read"],
|
||||
});
|
||||
return await findAllDeploymentsByComposeId(input.composeId);
|
||||
}),
|
||||
allByServer: protectedProcedure
|
||||
allByServer: withPermission("deployment", "read")
|
||||
.input(apiFindAllByServer)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this server",
|
||||
});
|
||||
}
|
||||
.query(async ({ input }) => {
|
||||
return await findAllDeploymentsByServerId(input.serverId);
|
||||
}),
|
||||
allCentralized: protectedProcedure.query(async ({ ctx }) => {
|
||||
const orgId = ctx.session.activeOrganizationId;
|
||||
const accessedServices =
|
||||
ctx.user.role === "member"
|
||||
? (await findMemberById(ctx.user.id, orgId)).accessedServices
|
||||
: null;
|
||||
if (accessedServices !== null && accessedServices.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return findAllDeploymentsCentralized(orgId, accessedServices);
|
||||
}),
|
||||
allCentralized: withPermission("deployment", "read").query(
|
||||
async ({ ctx }) => {
|
||||
const orgId = ctx.session.activeOrganizationId;
|
||||
const accessedServices =
|
||||
ctx.user.role !== "owner" && ctx.user.role !== "admin"
|
||||
? (await findMemberByUserId(ctx.user.id, orgId)).accessedServices
|
||||
: null;
|
||||
if (accessedServices !== null && accessedServices.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return findAllDeploymentsCentralized(orgId, accessedServices);
|
||||
},
|
||||
),
|
||||
|
||||
queueList: protectedProcedure.query(async ({ ctx }) => {
|
||||
queueList: withPermission("deployment", "read").query(async ({ ctx }) => {
|
||||
const orgId = ctx.session.activeOrganizationId;
|
||||
let rows: QueueJobRow[];
|
||||
|
||||
@@ -135,7 +117,10 @@ export const deploymentRouter = createTRPCRouter({
|
||||
|
||||
allByType: protectedProcedure
|
||||
.input(apiFindAllByType)
|
||||
.query(async ({ input }) => {
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||
deployment: ["read"],
|
||||
});
|
||||
const deploymentsList = await db.query.deployments.findMany({
|
||||
where: eq(deployments[`${input.type}Id`], input.id),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
@@ -151,8 +136,14 @@ export const deploymentRouter = createTRPCRouter({
|
||||
deploymentId: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const deployment = await findDeploymentById(input.deploymentId);
|
||||
const serviceId = deployment.applicationId || deployment.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
deployment: ["cancel"],
|
||||
});
|
||||
}
|
||||
|
||||
if (!deployment.pid) {
|
||||
throw new TRPCError({
|
||||
@@ -169,6 +160,11 @@ export const deploymentRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await audit(ctx, {
|
||||
action: "cancel",
|
||||
resourceType: "deployment",
|
||||
resourceId: deployment.deploymentId,
|
||||
});
|
||||
}),
|
||||
|
||||
removeDeployment: protectedProcedure
|
||||
@@ -177,7 +173,20 @@ export const deploymentRouter = createTRPCRouter({
|
||||
deploymentId: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await removeDeployment(input.deploymentId);
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const deployment = await findDeploymentById(input.deploymentId);
|
||||
const serviceId = deployment.applicationId || deployment.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
deployment: ["cancel"],
|
||||
});
|
||||
}
|
||||
const result = await removeDeployment(input.deploymentId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "deployment",
|
||||
resourceId: deployment.deploymentId,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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,31 +1,35 @@
|
||||
import {
|
||||
addNewEnvironment,
|
||||
checkEnvironmentAccess,
|
||||
checkEnvironmentCreationPermission,
|
||||
checkEnvironmentDeletionPermission,
|
||||
createEnvironment,
|
||||
deleteEnvironment,
|
||||
duplicateEnvironment,
|
||||
findEnvironmentById,
|
||||
findEnvironmentsByProjectId,
|
||||
findMemberById,
|
||||
updateEnvironmentById,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
addNewEnvironment,
|
||||
checkEnvironmentAccess,
|
||||
checkEnvironmentCreationPermission,
|
||||
checkEnvironmentDeletionPermission,
|
||||
checkPermission,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateEnvironment,
|
||||
apiDuplicateEnvironment,
|
||||
apiFindOneEnvironment,
|
||||
apiRemoveEnvironment,
|
||||
apiUpdateEnvironment,
|
||||
environments,
|
||||
projects,
|
||||
} from "@/server/db/schema";
|
||||
import { environments, projects } from "@/server/db/schema";
|
||||
|
||||
// Helper function to filter services within an environment based on user permissions
|
||||
const filterEnvironmentServices = (
|
||||
environment: any,
|
||||
accessedServices: string[],
|
||||
@@ -59,12 +63,7 @@ export const environmentRouter = createTRPCRouter({
|
||||
.input(apiCreateEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Check if user has permission to create environments
|
||||
await checkEnvironmentCreationPermission(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await checkEnvironmentCreationPermission(ctx, input.projectId);
|
||||
|
||||
if (input.name === "production") {
|
||||
throw new TRPCError({
|
||||
@@ -74,16 +73,15 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Allow users to create environments with any name, including "production"
|
||||
const environment = await createEnvironment(input);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewEnvironment(
|
||||
ctx.user.id,
|
||||
environment.environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
await addNewEnvironment(ctx, environment.environmentId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "environment",
|
||||
resourceId: environment.environmentId,
|
||||
resourceName: environment.name,
|
||||
});
|
||||
return environment;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
@@ -100,54 +98,39 @@ export const environmentRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneEnvironment)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
if (
|
||||
environment.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedEnvironments, accessedServices } =
|
||||
await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
input.environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
if (
|
||||
environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
|
||||
if (!accessedEnvironments.includes(environment.environmentId)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment access and filter services for members
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments, accessedServices } =
|
||||
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
|
||||
const filteredEnvironment = filterEnvironmentServices(
|
||||
environment,
|
||||
accessedServices,
|
||||
);
|
||||
|
||||
if (!accessedEnvironments.includes(environment.environmentId)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to access this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Filter services based on member permissions
|
||||
const filteredEnvironment = filterEnvironmentServices(
|
||||
environment,
|
||||
accessedServices,
|
||||
);
|
||||
|
||||
return filteredEnvironment;
|
||||
}
|
||||
|
||||
return environment;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Environment not found",
|
||||
});
|
||||
return filteredEnvironment;
|
||||
}
|
||||
|
||||
return environment;
|
||||
}),
|
||||
|
||||
byProjectId: protectedProcedure
|
||||
@@ -156,7 +139,6 @@ export const environmentRouter = createTRPCRouter({
|
||||
try {
|
||||
const environments = await findEnvironmentsByProjectId(input.projectId);
|
||||
|
||||
// Check organization access
|
||||
if (
|
||||
environments.some(
|
||||
(environment) =>
|
||||
@@ -170,12 +152,13 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Filter environments for members based on their permissions
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedEnvironments, accessedServices } =
|
||||
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
|
||||
await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
// Filter environments to only show those the member has access to
|
||||
const filteredEnvironments = environments
|
||||
.filter((environment) =>
|
||||
accessedEnvironments.includes(environment.environmentId),
|
||||
@@ -211,7 +194,6 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent deletion of the default environment
|
||||
if (environment.isDefault) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -219,24 +201,17 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment deletion permission
|
||||
await checkEnvironmentDeletionPermission(
|
||||
ctx.user.id,
|
||||
environment.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await checkEnvironmentDeletionPermission(ctx, environment.projectId);
|
||||
|
||||
// Additional check for environment access for members
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
ctx.user.id,
|
||||
input.environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkEnvironmentAccess(ctx, input.environmentId, "read");
|
||||
|
||||
const deletedEnvironment = await deleteEnvironment(input.environmentId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "environment",
|
||||
resourceId: deletedEnvironment?.environmentId,
|
||||
resourceName: deletedEnvironment?.name,
|
||||
});
|
||||
return deletedEnvironment;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
@@ -256,18 +231,14 @@ export const environmentRouter = createTRPCRouter({
|
||||
try {
|
||||
const { environmentId, ...updateData } = input;
|
||||
|
||||
// Allow users to rename environments to any name, including "production"
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
ctx.user.id,
|
||||
environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
await checkEnvironmentAccess(ctx, environmentId, "read");
|
||||
|
||||
if (updateData.env !== undefined) {
|
||||
await checkPermission(ctx, { environmentEnvVars: ["write"] });
|
||||
}
|
||||
|
||||
const currentEnvironment = await findEnvironmentById(environmentId);
|
||||
|
||||
// Prevent renaming the default environment, but allow updating env and description
|
||||
if (currentEnvironment.isDefault && updateData.name !== undefined) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -284,9 +255,8 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment access for members
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedEnvironments } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
@@ -305,6 +275,14 @@ export const environmentRouter = createTRPCRouter({
|
||||
environmentId,
|
||||
updateData,
|
||||
);
|
||||
if (environment) {
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "environment",
|
||||
resourceId: environment.environmentId,
|
||||
resourceName: environment.name,
|
||||
});
|
||||
}
|
||||
return environment;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -318,14 +296,7 @@ export const environmentRouter = createTRPCRouter({
|
||||
.input(apiDuplicateEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
ctx.user.id,
|
||||
input.environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkEnvironmentAccess(ctx, input.environmentId, "read");
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
if (
|
||||
environment.project.organizationId !==
|
||||
@@ -337,9 +308,8 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment access for members
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedEnvironments } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
@@ -353,6 +323,13 @@ export const environmentRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const duplicatedEnvironment = await duplicateEnvironment(input);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "environment",
|
||||
resourceId: duplicatedEnvironment.environmentId,
|
||||
resourceName: duplicatedEnvironment.name,
|
||||
metadata: { duplicatedFrom: input.environmentId },
|
||||
});
|
||||
return duplicatedEnvironment;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -404,8 +381,8 @@ export const environmentRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedEnvironments } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import {
|
||||
addNewService,
|
||||
checkPortInUse,
|
||||
checkServiceAccess,
|
||||
createMariadb,
|
||||
createMount,
|
||||
deployMariadb,
|
||||
findBackupsByDbId,
|
||||
findEnvironmentById,
|
||||
findMariadbById,
|
||||
findMemberById,
|
||||
findProjectById,
|
||||
IS_CLOUD,
|
||||
rebuildDatabase,
|
||||
@@ -21,11 +18,18 @@ import {
|
||||
updateMariadbById,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiChangeMariaDBStatus,
|
||||
apiCreateMariaDB,
|
||||
@@ -36,27 +40,20 @@ import {
|
||||
apiSaveEnvironmentVariablesMariaDB,
|
||||
apiSaveExternalPortMariaDB,
|
||||
apiUpdateMariaDB,
|
||||
environments,
|
||||
mariadb as mariadbTable,
|
||||
projects,
|
||||
} from "@/server/db/schema";
|
||||
import { environments, projects } from "@/server/db/schema";
|
||||
import { cancelJobs } from "@/server/utils/backup";
|
||||
export const mariadbRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, project.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -74,13 +71,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
const newMariadb = await createMariadb({
|
||||
...input,
|
||||
});
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newMariadb.mariadbId,
|
||||
project.organizationId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, newMariadb.mariadbId);
|
||||
|
||||
await createMount({
|
||||
serviceId: newMariadb.mariadbId,
|
||||
@@ -90,6 +81,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "service",
|
||||
resourceId: newMariadb.mariadbId,
|
||||
resourceName: newMariadb.appName,
|
||||
});
|
||||
return newMariadb;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
@@ -101,14 +98,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mariadbId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.mariadbId, "read");
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
@@ -125,16 +115,10 @@ export const mariadbRouter = createTRPCRouter({
|
||||
start: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const service = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this Mariadb",
|
||||
});
|
||||
}
|
||||
if (service.serverId) {
|
||||
await startServiceRemote(service.serverId, service.appName);
|
||||
} else {
|
||||
@@ -144,11 +128,20 @@ export const mariadbRouter = createTRPCRouter({
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "service",
|
||||
resourceId: service.mariadbId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
|
||||
if (mariadb.serverId) {
|
||||
@@ -160,21 +153,21 @@ export const mariadbRouter = createTRPCRouter({
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "service",
|
||||
resourceId: mariadb.mariadbId,
|
||||
resourceName: mariadb.appName,
|
||||
});
|
||||
return mariadb;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.input(apiSaveExternalPortMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this external port",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.externalPort) {
|
||||
const portCheck = await checkPortInUse(
|
||||
@@ -193,22 +186,28 @@ export const mariadbRouter = createTRPCRouter({
|
||||
externalPort: input.externalPort,
|
||||
});
|
||||
await deployMariadb(input.mariadbId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mariadb.mariadbId,
|
||||
resourceName: mariadb.appName,
|
||||
});
|
||||
return mariadb;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Mariadb",
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "service",
|
||||
resourceId: mariadb.mariadbId,
|
||||
resourceName: mariadb.appName,
|
||||
});
|
||||
return deployMariadb(input.mariadbId);
|
||||
}),
|
||||
deployWithLogs: protectedProcedure
|
||||
@@ -222,16 +221,9 @@ export const mariadbRouter = createTRPCRouter({
|
||||
})
|
||||
.input(apiDeployMariaDB)
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Mariadb",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
return observable<string>((emit) => {
|
||||
deployMariadb(input.mariadbId, (log) => {
|
||||
@@ -242,32 +234,25 @@ export const mariadbRouter = createTRPCRouter({
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeMariaDBStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to change this Mariadb status",
|
||||
});
|
||||
}
|
||||
await updateMariadbById(input.mariadbId, {
|
||||
applicationStatus: input.applicationStatus,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mariadbId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mariadbId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"delete",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.mariadbId, "delete");
|
||||
|
||||
const mongo = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
@@ -280,6 +265,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mariadbId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
const backups = await findBackupsByDbId(input.mariadbId, "mariadb");
|
||||
const cleanupOperations = [
|
||||
async () => await removeService(mongo?.appName, mongo.serverId),
|
||||
@@ -298,16 +289,9 @@ export const mariadbRouter = createTRPCRouter({
|
||||
saveEnvironment: protectedProcedure
|
||||
.input(apiSaveEnvironmentVariablesMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
envVars: ["write"],
|
||||
});
|
||||
const service = await updateMariadbById(input.mariadbId, {
|
||||
env: input.env,
|
||||
});
|
||||
@@ -319,21 +303,20 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: input.mariadbId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetMariadb)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this Mariadb",
|
||||
});
|
||||
}
|
||||
if (mariadb.serverId) {
|
||||
await stopServiceRemote(mariadb.serverId, mariadb.appName);
|
||||
} else {
|
||||
@@ -351,22 +334,21 @@ export const mariadbRouter = createTRPCRouter({
|
||||
await updateMariadbById(input.mariadbId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "reload",
|
||||
resourceType: "service",
|
||||
resourceId: mariadb.mariadbId,
|
||||
resourceName: mariadb.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mariadbId, ...rest } = input;
|
||||
const mariadb = await findMariadbById(mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this Mariadb",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, mariadbId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const service = await updateMariadbById(mariadbId, {
|
||||
...rest,
|
||||
});
|
||||
@@ -378,6 +360,12 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mariadbId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
@@ -388,31 +376,10 @@ export const mariadbRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this mariadb",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
const targetEnvironment = await findEnvironmentById(
|
||||
input.targetEnvironmentId,
|
||||
);
|
||||
if (
|
||||
targetEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Update the mariadb's projectId
|
||||
const updatedMariadb = await db
|
||||
.update(mariadbTable)
|
||||
.set({
|
||||
@@ -429,23 +396,27 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "move",
|
||||
resourceType: "service",
|
||||
resourceId: updatedMariadb.mariadbId,
|
||||
resourceName: updatedMariadb.appName,
|
||||
});
|
||||
return updatedMariadb;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.input(apiRebuildMariadb)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mariadb = await findMariadbById(input.mariadbId);
|
||||
if (
|
||||
mariadb.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to rebuild this MariaDB database",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mariadbId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
await rebuildDatabase(mariadb.mariadbId, "mariadb");
|
||||
await rebuildDatabase(input.mariadbId, "mariadb");
|
||||
await audit(ctx, {
|
||||
action: "rebuild",
|
||||
resourceType: "service",
|
||||
resourceId: input.mariadbId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
@@ -499,19 +470,18 @@ export const mariadbRouter = createTRPCRouter({
|
||||
),
|
||||
);
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mariadbTable.mariadbId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const { accessedServices } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mariadbTable.mariadbId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
|
||||
const where = and(...baseConditions);
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import {
|
||||
addNewService,
|
||||
checkPortInUse,
|
||||
checkServiceAccess,
|
||||
createMongo,
|
||||
createMount,
|
||||
deployMongo,
|
||||
findBackupsByDbId,
|
||||
findEnvironmentById,
|
||||
findMemberById,
|
||||
findMongoById,
|
||||
findProjectById,
|
||||
IS_CLOUD,
|
||||
@@ -20,10 +17,17 @@ import {
|
||||
stopServiceRemote,
|
||||
updateMongoById,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiChangeMongoStatus,
|
||||
@@ -44,18 +48,10 @@ export const mongoRouter = createTRPCRouter({
|
||||
.input(apiCreateMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, project.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -73,13 +69,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
const newMongo = await createMongo({
|
||||
...input,
|
||||
});
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newMongo.mongoId,
|
||||
project.organizationId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, newMongo.mongoId);
|
||||
|
||||
await createMount({
|
||||
serviceId: newMongo.mongoId,
|
||||
@@ -89,6 +79,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "service",
|
||||
resourceId: newMongo.mongoId,
|
||||
resourceName: newMongo.appName,
|
||||
});
|
||||
return newMongo;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
@@ -104,14 +100,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMongo)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mongoId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.mongoId, "read");
|
||||
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
@@ -129,18 +118,11 @@ export const mongoRouter = createTRPCRouter({
|
||||
start: protectedProcedure
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const service = await findMongoById(input.mongoId);
|
||||
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this mongo",
|
||||
});
|
||||
}
|
||||
|
||||
if (service.serverId) {
|
||||
await startServiceRemote(service.serverId, service.appName);
|
||||
} else {
|
||||
@@ -150,23 +132,22 @@ export const mongoRouter = createTRPCRouter({
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "service",
|
||||
resourceId: service.mongoId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this mongo",
|
||||
});
|
||||
}
|
||||
|
||||
if (mongo.serverId) {
|
||||
await stopServiceRemote(mongo.serverId, mongo.appName);
|
||||
} else {
|
||||
@@ -176,21 +157,21 @@ export const mongoRouter = createTRPCRouter({
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mongoId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.input(apiSaveExternalPortMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this external port",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.externalPort) {
|
||||
const portCheck = await checkPortInUse(
|
||||
@@ -209,21 +190,27 @@ export const mongoRouter = createTRPCRouter({
|
||||
externalPort: input.externalPort,
|
||||
});
|
||||
await deployMongo(input.mongoId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mongoId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this mongo",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mongoId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return deployMongo(input.mongoId);
|
||||
}),
|
||||
deployWithLogs: protectedProcedure
|
||||
@@ -237,16 +224,9 @@ export const mongoRouter = createTRPCRouter({
|
||||
})
|
||||
.input(apiDeployMongo)
|
||||
.subscription(async function* ({ input, ctx, signal }) {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this mongo",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
|
||||
@@ -270,34 +250,28 @@ export const mongoRouter = createTRPCRouter({
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeMongoStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to change this mongo status",
|
||||
});
|
||||
}
|
||||
await updateMongoById(input.mongoId, {
|
||||
applicationStatus: input.applicationStatus,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mongoId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this mongo",
|
||||
});
|
||||
}
|
||||
if (mongo.serverId) {
|
||||
await stopServiceRemote(mongo.serverId, mongo.appName);
|
||||
} else {
|
||||
@@ -315,19 +289,18 @@ export const mongoRouter = createTRPCRouter({
|
||||
await updateMongoById(input.mongoId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "reload",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mongoId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mongoId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"delete",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.mongoId, "delete");
|
||||
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
|
||||
@@ -340,6 +313,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
message: "You are not authorized to delete this mongo",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mongoId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
const backups = await findBackupsByDbId(input.mongoId, "mongo");
|
||||
|
||||
const cleanupOperations = [
|
||||
@@ -359,16 +338,9 @@ export const mongoRouter = createTRPCRouter({
|
||||
saveEnvironment: protectedProcedure
|
||||
.input(apiSaveEnvironmentVariablesMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
envVars: ["write"],
|
||||
});
|
||||
const service = await updateMongoById(input.mongoId, {
|
||||
env: input.env,
|
||||
});
|
||||
@@ -380,22 +352,20 @@ export const mongoRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: input.mongoId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mongoId, ...rest } = input;
|
||||
const mongo = await findMongoById(mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this mongo",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, mongoId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const service = await updateMongoById(mongoId, {
|
||||
...rest,
|
||||
});
|
||||
@@ -407,6 +377,12 @@ export const mongoRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mongoId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
@@ -417,31 +393,10 @@ export const mongoRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this mongo",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
const targetEnvironment = await findEnvironmentById(
|
||||
input.targetEnvironmentId,
|
||||
);
|
||||
if (
|
||||
targetEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Update the mongo's projectId
|
||||
const updatedMongo = await db
|
||||
.update(mongoTable)
|
||||
.set({
|
||||
@@ -458,24 +413,28 @@ export const mongoRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "move",
|
||||
resourceType: "service",
|
||||
resourceId: updatedMongo.mongoId,
|
||||
resourceName: updatedMongo.appName,
|
||||
});
|
||||
return updatedMongo;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.input(apiRebuildMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mongo = await findMongoById(input.mongoId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to rebuild this MongoDB database",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mongoId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
await rebuildDatabase(mongo.mongoId, "mongo");
|
||||
await rebuildDatabase(input.mongoId, "mongo");
|
||||
|
||||
await audit(ctx, {
|
||||
action: "rebuild",
|
||||
resourceType: "service",
|
||||
resourceId: input.mongoId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
@@ -524,19 +483,18 @@ export const mongoRouter = createTRPCRouter({
|
||||
ilike(mongoTable.description ?? "", `%${input.description.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mongoTable.mongoId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const { accessedServices } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mongoTable.mongoId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
|
||||
const where = and(...baseConditions);
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
checkServiceAccess,
|
||||
createMount,
|
||||
deleteMount,
|
||||
findApplicationById,
|
||||
@@ -7,7 +6,6 @@ import {
|
||||
findMariadbById,
|
||||
findMongoById,
|
||||
findMountById,
|
||||
findMountOrganizationId,
|
||||
findMountsByApplicationId,
|
||||
findMySqlById,
|
||||
findPostgresById,
|
||||
@@ -15,6 +13,10 @@ import {
|
||||
getServiceContainer,
|
||||
updateMount,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import type { ServiceType } from "@dokploy/server/db/schema/mount";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
@@ -26,6 +28,7 @@ import {
|
||||
apiUpdateMount,
|
||||
} from "@/server/db/schema";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
|
||||
async function getServiceOrganizationId(
|
||||
serviceId: string,
|
||||
@@ -68,49 +71,94 @@ async function getServiceOrganizationId(
|
||||
export const mountRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateMount)
|
||||
.mutation(async ({ input }) => {
|
||||
return await createMount(input);
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.serviceId, {
|
||||
volume: ["create"],
|
||||
});
|
||||
const mount = await createMount(input);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "mount",
|
||||
resourceId: mount.mountId,
|
||||
resourceName: input.mountPath,
|
||||
});
|
||||
return mount;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiRemoveMount)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const organizationId = await findMountOrganizationId(input.mountId);
|
||||
if (organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this mount",
|
||||
const mount = await findMountById(input.mountId);
|
||||
const serviceId =
|
||||
mount.applicationId ||
|
||||
mount.postgresId ||
|
||||
mount.mariadbId ||
|
||||
mount.mongoId ||
|
||||
mount.mysqlId ||
|
||||
mount.redisId ||
|
||||
mount.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
volume: ["delete"],
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "mount",
|
||||
resourceId: input.mountId,
|
||||
});
|
||||
return await deleteMount(input.mountId);
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMount)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const organizationId = await findMountOrganizationId(input.mountId);
|
||||
if (organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this mount",
|
||||
const mount = await findMountById(input.mountId);
|
||||
const serviceId =
|
||||
mount.applicationId ||
|
||||
mount.postgresId ||
|
||||
mount.mariadbId ||
|
||||
mount.mongoId ||
|
||||
mount.mysqlId ||
|
||||
mount.redisId ||
|
||||
mount.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
volume: ["read"],
|
||||
});
|
||||
}
|
||||
return await findMountById(input.mountId);
|
||||
return mount;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateMount)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const organizationId = await findMountOrganizationId(input.mountId);
|
||||
if (organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this mount",
|
||||
const mount = await findMountById(input.mountId);
|
||||
const serviceId =
|
||||
mount.applicationId ||
|
||||
mount.postgresId ||
|
||||
mount.mariadbId ||
|
||||
mount.mongoId ||
|
||||
mount.mysqlId ||
|
||||
mount.redisId ||
|
||||
mount.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
volume: ["create"],
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "mount",
|
||||
resourceId: input.mountId,
|
||||
resourceName: input.mountPath,
|
||||
});
|
||||
return await updateMount(input.mountId, input);
|
||||
}),
|
||||
allNamedByApplicationId: protectedProcedure
|
||||
.input(z.object({ applicationId: z.string().min(1) }))
|
||||
.query(async ({ input }) => {
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.applicationId, {
|
||||
volume: ["read"],
|
||||
});
|
||||
const app = await findApplicationById(input.applicationId);
|
||||
const container = await getServiceContainer(app.appName, app.serverId);
|
||||
const mounts = container?.Mounts.filter(
|
||||
@@ -122,14 +170,7 @@ export const mountRouter = createTRPCRouter({
|
||||
.input(apiFindMountByApplicationId)
|
||||
.query(async ({ input, ctx }) => {
|
||||
console.log("input", input);
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.serviceId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.serviceId, "read");
|
||||
const organizationId = await getServiceOrganizationId(
|
||||
input.serviceId,
|
||||
input.serviceType,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import {
|
||||
addNewService,
|
||||
checkPortInUse,
|
||||
checkServiceAccess,
|
||||
createMount,
|
||||
createMysql,
|
||||
deployMySql,
|
||||
findBackupsByDbId,
|
||||
findEnvironmentById,
|
||||
findMemberById,
|
||||
findMySqlById,
|
||||
findProjectById,
|
||||
IS_CLOUD,
|
||||
@@ -20,10 +17,17 @@ import {
|
||||
stopServiceRemote,
|
||||
updateMySqlById,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiChangeMySqlStatus,
|
||||
@@ -46,18 +50,10 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(apiCreateMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, project.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -76,13 +72,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
const newMysql = await createMysql({
|
||||
...input,
|
||||
});
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newMysql.mysqlId,
|
||||
project.organizationId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, newMysql.mysqlId);
|
||||
|
||||
await createMount({
|
||||
serviceId: newMysql.mysqlId,
|
||||
@@ -92,6 +82,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "service",
|
||||
resourceId: newMysql.mysqlId,
|
||||
resourceName: newMysql.appName,
|
||||
});
|
||||
return newMysql;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
@@ -107,14 +103,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMySql)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mysqlId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.mysqlId, "read");
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
@@ -131,16 +120,10 @@ export const mysqlRouter = createTRPCRouter({
|
||||
start: protectedProcedure
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const service = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this MySQL",
|
||||
});
|
||||
}
|
||||
|
||||
if (service.serverId) {
|
||||
await startServiceRemote(service.serverId, service.appName);
|
||||
@@ -151,21 +134,21 @@ export const mysqlRouter = createTRPCRouter({
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "service",
|
||||
resourceId: service.mysqlId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this MySQL",
|
||||
});
|
||||
}
|
||||
if (mongo.serverId) {
|
||||
await stopServiceRemote(mongo.serverId, mongo.appName);
|
||||
} else {
|
||||
@@ -175,21 +158,21 @@ export const mysqlRouter = createTRPCRouter({
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mysqlId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.input(apiSaveExternalPortMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this external port",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.externalPort) {
|
||||
const portCheck = await checkPortInUse(
|
||||
@@ -208,21 +191,27 @@ export const mysqlRouter = createTRPCRouter({
|
||||
externalPort: input.externalPort,
|
||||
});
|
||||
await deployMySql(input.mysqlId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mysql.mysqlId,
|
||||
resourceName: mysql.appName,
|
||||
});
|
||||
return mysql;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this MySQL",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "service",
|
||||
resourceId: mysql.mysqlId,
|
||||
resourceName: mysql.appName,
|
||||
});
|
||||
return deployMySql(input.mysqlId);
|
||||
}),
|
||||
deployWithLogs: protectedProcedure
|
||||
@@ -236,16 +225,9 @@ export const mysqlRouter = createTRPCRouter({
|
||||
})
|
||||
.input(apiDeployMySql)
|
||||
.subscription(async function* ({ input, ctx, signal }) {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this MySQL",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
@@ -269,34 +251,28 @@ export const mysqlRouter = createTRPCRouter({
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeMySqlStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to change this MySQL status",
|
||||
});
|
||||
}
|
||||
await updateMySqlById(input.mysqlId, {
|
||||
applicationStatus: input.applicationStatus,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mysqlId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetMysql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this MySQL",
|
||||
});
|
||||
}
|
||||
if (mysql.serverId) {
|
||||
await stopServiceRemote(mysql.serverId, mysql.appName);
|
||||
} else {
|
||||
@@ -313,19 +289,18 @@ export const mysqlRouter = createTRPCRouter({
|
||||
await updateMySqlById(input.mysqlId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "reload",
|
||||
resourceType: "service",
|
||||
resourceId: mysql.mysqlId,
|
||||
resourceName: mysql.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mysqlId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"delete",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.mysqlId, "delete");
|
||||
const mongo = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
@@ -337,6 +312,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.mysqlId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
const backups = await findBackupsByDbId(input.mysqlId, "mysql");
|
||||
const cleanupOperations = [
|
||||
async () => await removeService(mongo?.appName, mongo.serverId),
|
||||
@@ -355,16 +336,9 @@ export const mysqlRouter = createTRPCRouter({
|
||||
saveEnvironment: protectedProcedure
|
||||
.input(apiSaveEnvironmentVariablesMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
envVars: ["write"],
|
||||
});
|
||||
const service = await updateMySqlById(input.mysqlId, {
|
||||
env: input.env,
|
||||
});
|
||||
@@ -376,22 +350,20 @@ export const mysqlRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: input.mysqlId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { mysqlId, ...rest } = input;
|
||||
const mysql = await findMySqlById(mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this MySQL",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, mysqlId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const service = await updateMySqlById(mysqlId, {
|
||||
...rest,
|
||||
});
|
||||
@@ -403,6 +375,12 @@ export const mysqlRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mysqlId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
@@ -413,31 +391,10 @@ export const mysqlRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this mysql",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
const targetEnvironment = await findEnvironmentById(
|
||||
input.targetEnvironmentId,
|
||||
);
|
||||
if (
|
||||
targetEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Update the mysql's projectId
|
||||
const updatedMysql = await db
|
||||
.update(mysqlTable)
|
||||
.set({
|
||||
@@ -454,24 +411,28 @@ export const mysqlRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "move",
|
||||
resourceType: "service",
|
||||
resourceId: updatedMysql.mysqlId,
|
||||
resourceName: updatedMysql.appName,
|
||||
});
|
||||
return updatedMysql;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.input(apiRebuildMysql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const mysql = await findMySqlById(input.mysqlId);
|
||||
if (
|
||||
mysql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to rebuild this MySQL database",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.mysqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
await rebuildDatabase(mysql.mysqlId, "mysql");
|
||||
await rebuildDatabase(input.mysqlId, "mysql");
|
||||
|
||||
await audit(ctx, {
|
||||
action: "rebuild",
|
||||
resourceType: "service",
|
||||
resourceId: input.mysqlId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
@@ -520,19 +481,18 @@ export const mysqlRouter = createTRPCRouter({
|
||||
ilike(mysqlTable.description ?? "", `%${input.description.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mysqlTable.mysqlId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const { accessedServices } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mysqlTable.mysqlId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
|
||||
const where = and(...baseConditions);
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
|
||||
@@ -43,11 +43,11 @@ import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateCustom,
|
||||
apiCreateDiscord,
|
||||
@@ -88,15 +88,18 @@ import {
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const notificationRouter = createTRPCRouter({
|
||||
createSlack: adminProcedure
|
||||
createSlack: withPermission("notification", "create")
|
||||
.input(apiCreateSlack)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createSlackNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createSlackNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the notification",
|
||||
@@ -104,7 +107,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateSlack: adminProcedure
|
||||
updateSlack: withPermission("notification", "update")
|
||||
.input(apiUpdateSlack)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -115,15 +118,22 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateSlackNotification({
|
||||
const result = await updateSlackNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testSlackConnection: adminProcedure
|
||||
testSlackConnection: withPermission("notification", "create")
|
||||
.input(apiTestSlackConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -140,14 +150,19 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createTelegram: adminProcedure
|
||||
createTelegram: withPermission("notification", "create")
|
||||
.input(apiCreateTelegram)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createTelegramNotification(
|
||||
await createTelegramNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -157,7 +172,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
updateTelegram: adminProcedure
|
||||
updateTelegram: withPermission("notification", "update")
|
||||
.input(apiUpdateTelegram)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -168,10 +183,17 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateTelegramNotification({
|
||||
const result = await updateTelegramNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -180,7 +202,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
testTelegramConnection: adminProcedure
|
||||
testTelegramConnection: withPermission("notification", "create")
|
||||
.input(apiTestTelegramConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -194,14 +216,19 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createDiscord: adminProcedure
|
||||
createDiscord: withPermission("notification", "create")
|
||||
.input(apiCreateDiscord)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createDiscordNotification(
|
||||
await createDiscordNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -211,7 +238,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
updateDiscord: adminProcedure
|
||||
updateDiscord: withPermission("notification", "update")
|
||||
.input(apiUpdateDiscord)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -222,10 +249,17 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateDiscordNotification({
|
||||
const result = await updateDiscordNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -235,7 +269,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
testDiscordConnection: adminProcedure
|
||||
testDiscordConnection: withPermission("notification", "create")
|
||||
.input(apiTestDiscordConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -257,14 +291,16 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createEmail: adminProcedure
|
||||
createEmail: withPermission("notification", "create")
|
||||
.input(apiCreateEmail)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createEmailNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createEmailNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -273,7 +309,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateEmail: adminProcedure
|
||||
updateEmail: withPermission("notification", "update")
|
||||
.input(apiUpdateEmail)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -284,10 +320,17 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateEmailNotification({
|
||||
const result = await updateEmailNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -296,7 +339,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
testEmailConnection: adminProcedure
|
||||
testEmailConnection: withPermission("notification", "create")
|
||||
.input(apiTestEmailConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -314,14 +357,16 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createResend: adminProcedure
|
||||
createResend: withPermission("notification", "create")
|
||||
.input(apiCreateResend)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createResendNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createResendNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -330,7 +375,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateResend: adminProcedure
|
||||
updateResend: withPermission("notification", "update")
|
||||
.input(apiUpdateResend)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -341,10 +386,17 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateResendNotification({
|
||||
const result = await updateResendNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -353,7 +405,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
testResendConnection: adminProcedure
|
||||
testResendConnection: withPermission("notification", "create")
|
||||
.input(apiTestResendConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -371,7 +423,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
remove: adminProcedure
|
||||
remove: withPermission("notification", "delete")
|
||||
.input(apiFindOneNotification)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -382,6 +434,11 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to delete this notification",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "notification",
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return await removeNotificationById(input.notificationId);
|
||||
} catch (error) {
|
||||
const message =
|
||||
@@ -394,7 +451,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
one: withPermission("notification", "read")
|
||||
.input(apiFindOneNotification)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const notification = await findNotificationById(input.notificationId);
|
||||
@@ -406,7 +463,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
}
|
||||
return notification;
|
||||
}),
|
||||
all: adminProcedure.query(async ({ ctx }) => {
|
||||
all: withPermission("notification", "read").query(async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
with: {
|
||||
slack: true,
|
||||
@@ -453,8 +510,6 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// For Dokploy server type, we don't have a specific organizationId
|
||||
// This might need to be adjusted based on your business logic
|
||||
organizationId = "";
|
||||
ServerName = "Dokploy";
|
||||
} else {
|
||||
@@ -488,14 +543,16 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createGotify: adminProcedure
|
||||
createGotify: withPermission("notification", "create")
|
||||
.input(apiCreateGotify)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createGotifyNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createGotifyNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -504,7 +561,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateGotify: adminProcedure
|
||||
updateGotify: withPermission("notification", "update")
|
||||
.input(apiUpdateGotify)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -518,15 +575,22 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateGotifyNotification({
|
||||
const result = await updateGotifyNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testGotifyConnection: adminProcedure
|
||||
testGotifyConnection: withPermission("notification", "create")
|
||||
.input(apiTestGotifyConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -544,14 +608,16 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createNtfy: adminProcedure
|
||||
createNtfy: withPermission("notification", "create")
|
||||
.input(apiCreateNtfy)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createNtfyNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createNtfyNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -560,7 +626,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateNtfy: adminProcedure
|
||||
updateNtfy: withPermission("notification", "update")
|
||||
.input(apiUpdateNtfy)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -574,15 +640,22 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateNtfyNotification({
|
||||
const result = await updateNtfyNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testNtfyConnection: adminProcedure
|
||||
testNtfyConnection: withPermission("notification", "create")
|
||||
.input(apiTestNtfyConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -602,14 +675,16 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createCustom: adminProcedure
|
||||
createCustom: withPermission("notification", "create")
|
||||
.input(apiCreateCustom)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createCustomNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createCustomNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -618,7 +693,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateCustom: adminProcedure
|
||||
updateCustom: withPermission("notification", "update")
|
||||
.input(apiUpdateCustom)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -629,15 +704,22 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateCustomNotification({
|
||||
const result = await updateCustomNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testCustomConnection: adminProcedure
|
||||
testCustomConnection: withPermission("notification", "create")
|
||||
.input(apiTestCustomConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -655,14 +737,16 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createLark: adminProcedure
|
||||
createLark: withPermission("notification", "create")
|
||||
.input(apiCreateLark)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createLarkNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createLarkNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -671,7 +755,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateLark: adminProcedure
|
||||
updateLark: withPermission("notification", "update")
|
||||
.input(apiUpdateLark)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -685,15 +769,22 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateLarkNotification({
|
||||
const result = await updateLarkNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testLarkConnection: adminProcedure
|
||||
testLarkConnection: withPermission("notification", "create")
|
||||
.input(apiTestLarkConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -712,14 +803,16 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createTeams: adminProcedure
|
||||
createTeams: withPermission("notification", "create")
|
||||
.input(apiCreateTeams)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createTeamsNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await createTeamsNotification(input, ctx.session.activeOrganizationId);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -728,7 +821,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateTeams: adminProcedure
|
||||
updateTeams: withPermission("notification", "update")
|
||||
.input(apiUpdateTeams)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -742,15 +835,22 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updateTeamsNotification({
|
||||
const result = await updateTeamsNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testTeamsConnection: adminProcedure
|
||||
testTeamsConnection: withPermission("notification", "create")
|
||||
.input(apiTestTeamsConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -767,14 +867,19 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createPushover: adminProcedure
|
||||
createPushover: withPermission("notification", "create")
|
||||
.input(apiCreatePushover)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createPushoverNotification(
|
||||
await createPushoverNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -783,7 +888,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
updatePushover: adminProcedure
|
||||
updatePushover: withPermission("notification", "update")
|
||||
.input(apiUpdatePushover)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -797,15 +902,22 @@ export const notificationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
return await updatePushoverNotification({
|
||||
const result = await updatePushoverNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testPushoverConnection: adminProcedure
|
||||
testPushoverConnection: withPermission("notification", "create")
|
||||
.input(apiTestPushoverConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
@@ -823,13 +935,18 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
||||
with: {
|
||||
email: true,
|
||||
resend: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
getEmailProviders: withPermission("notification", "read").query(
|
||||
async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
where: eq(
|
||||
notifications.organizationId,
|
||||
ctx.session.activeOrganizationId,
|
||||
),
|
||||
with: {
|
||||
email: true,
|
||||
resend: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { IS_CLOUD } from "@dokploy/server/index";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, exists } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { invitation, member, organization } from "@/server/db/schema";
|
||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import {
|
||||
invitation,
|
||||
member,
|
||||
organization,
|
||||
organizationRole,
|
||||
user,
|
||||
} from "@/server/db/schema";
|
||||
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
export const organizationRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
@@ -50,6 +57,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
createdAt: new Date(),
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "organization",
|
||||
resourceId: result.id,
|
||||
resourceName: result.name,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
@@ -156,6 +169,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
})
|
||||
.where(eq(organization.id, input.organizationId))
|
||||
.returning();
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "organization",
|
||||
resourceId: input.organizationId,
|
||||
resourceName: input.name,
|
||||
});
|
||||
return result[0];
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
@@ -220,15 +239,109 @@ export const organizationRouter = createTRPCRouter({
|
||||
.delete(organization)
|
||||
.where(eq(organization.id, input.organizationId));
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "organization",
|
||||
resourceId: input.organizationId,
|
||||
resourceName: org.name,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
allInvitations: adminProcedure.query(async ({ ctx }) => {
|
||||
inviteMember: withPermission("member", "create")
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
role: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const orgId = ctx.session.activeOrganizationId;
|
||||
const email = input.email.toLowerCase();
|
||||
|
||||
// Check if user is already a member
|
||||
const existingUser = await db.query.user.findFirst({
|
||||
where: eq(user.email, email),
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
const existingMember = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.organizationId, orgId),
|
||||
eq(member.userId, existingUser.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (existingMember) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "User is already a member of this organization",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for pending invitation
|
||||
const existingInvitation = await db.query.invitation.findFirst({
|
||||
where: and(
|
||||
eq(invitation.organizationId, orgId),
|
||||
eq(invitation.email, email),
|
||||
eq(invitation.status, "pending"),
|
||||
),
|
||||
});
|
||||
|
||||
if (existingInvitation) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "An invitation has already been sent to this email",
|
||||
});
|
||||
}
|
||||
|
||||
// If assigning a custom role, verify it exists
|
||||
if (!["owner", "admin", "member"].includes(input.role)) {
|
||||
const customRole = await db.query.organizationRole.findFirst({
|
||||
where: and(
|
||||
eq(organizationRole.organizationId, orgId),
|
||||
eq(organizationRole.role, input.role),
|
||||
),
|
||||
});
|
||||
|
||||
if (!customRole) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: `Role "${input.role}" not found`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const [created] = await db
|
||||
.insert(invitation)
|
||||
.values({
|
||||
id: nanoid(),
|
||||
organizationId: orgId,
|
||||
email,
|
||||
role: input.role as any,
|
||||
status: "pending",
|
||||
expiresAt: new Date(Date.now() + 48 * 60 * 60 * 1000),
|
||||
inviterId: ctx.user.id,
|
||||
})
|
||||
.returning();
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "organization",
|
||||
resourceId: created?.id,
|
||||
resourceName: email,
|
||||
metadata: { type: "inviteMember", role: input.role },
|
||||
});
|
||||
return created;
|
||||
}),
|
||||
|
||||
allInvitations: withPermission("member", "create").query(async ({ ctx }) => {
|
||||
return await db.query.invitation.findMany({
|
||||
where: eq(invitation.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: [desc(invitation.status), desc(invitation.expiresAt)],
|
||||
});
|
||||
}),
|
||||
removeInvitation: adminProcedure
|
||||
removeInvitation: withPermission("member", "create")
|
||||
.input(z.object({ invitationId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const invitationResult = await db.query.invitation.findFirst({
|
||||
@@ -251,15 +364,23 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
return await db
|
||||
const result = await db
|
||||
.delete(invitation)
|
||||
.where(eq(invitation.id, input.invitationId));
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "organization",
|
||||
resourceId: input.invitationId,
|
||||
resourceName: invitationResult.email,
|
||||
metadata: { type: "removeInvitation" },
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
updateMemberRole: adminProcedure
|
||||
updateMemberRole: withPermission("member", "update")
|
||||
.input(
|
||||
z.object({
|
||||
memberId: z.string(),
|
||||
role: z.enum(["admin", "member"]),
|
||||
role: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
@@ -289,7 +410,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
// Owner role is intransferible - cannot change to or from owner
|
||||
if (target.role === "owner") {
|
||||
if (target.role === "owner" || input.role === "owner") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "The owner role is intransferible",
|
||||
@@ -306,12 +427,39 @@ export const organizationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// If assigning a custom role (not admin/member), verify it exists
|
||||
if (input.role !== "admin" && input.role !== "member") {
|
||||
const customRole = await db.query.organizationRole.findFirst({
|
||||
where: and(
|
||||
eq(
|
||||
organizationRole.organizationId,
|
||||
ctx.session.activeOrganizationId,
|
||||
),
|
||||
eq(organizationRole.role, input.role),
|
||||
),
|
||||
});
|
||||
|
||||
if (!customRole) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: `Custom role "${input.role}" not found`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update the target member's role
|
||||
await db
|
||||
.update(member)
|
||||
.set({ role: input.role })
|
||||
.where(eq(member.id, input.memberId));
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "user",
|
||||
resourceId: target.userId,
|
||||
resourceName: target.user.email,
|
||||
metadata: { before: target.role, after: input.role },
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
setDefault: protectedProcedure
|
||||
@@ -353,6 +501,12 @@ export const organizationRouter = createTRPCRouter({
|
||||
),
|
||||
);
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "organization",
|
||||
resourceId: input.organizationId,
|
||||
metadata: { type: "setDefault" },
|
||||
});
|
||||
return { success: true };
|
||||
}),
|
||||
active: protectedProcedure.query(async ({ ctx }) => {
|
||||
|
||||
@@ -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,13 +1,10 @@
|
||||
import {
|
||||
addNewService,
|
||||
checkPortInUse,
|
||||
checkServiceAccess,
|
||||
createMount,
|
||||
createPostgres,
|
||||
deployPostgres,
|
||||
findBackupsByDbId,
|
||||
findEnvironmentById,
|
||||
findMemberById,
|
||||
findPostgresById,
|
||||
findProjectById,
|
||||
getMountPath,
|
||||
@@ -21,10 +18,17 @@ import {
|
||||
stopServiceRemote,
|
||||
updatePostgresById,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiChangePostgresStatus,
|
||||
@@ -46,18 +50,10 @@ export const postgresRouter = createTRPCRouter({
|
||||
.input(apiCreatePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, project.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -75,13 +71,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
const newPostgres = await createPostgres({
|
||||
...input,
|
||||
});
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newPostgres.postgresId,
|
||||
project.organizationId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, newPostgres.postgresId);
|
||||
|
||||
const mountPath = getMountPath(input.dockerImage);
|
||||
|
||||
@@ -93,6 +83,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "service",
|
||||
resourceId: newPostgres.postgresId,
|
||||
resourceName: newPostgres.appName,
|
||||
});
|
||||
return newPostgres;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
@@ -108,14 +104,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOnePostgres)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.postgresId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.postgresId, "read");
|
||||
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
@@ -133,18 +122,11 @@ export const postgresRouter = createTRPCRouter({
|
||||
start: protectedProcedure
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const service = await findPostgresById(input.postgresId);
|
||||
|
||||
if (
|
||||
service.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this Postgres",
|
||||
});
|
||||
}
|
||||
|
||||
if (service.serverId) {
|
||||
await startServiceRemote(service.serverId, service.appName);
|
||||
} else {
|
||||
@@ -154,21 +136,21 @@ export const postgresRouter = createTRPCRouter({
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "service",
|
||||
resourceId: service.postgresId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return service;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this Postgres",
|
||||
});
|
||||
}
|
||||
if (postgres.serverId) {
|
||||
await stopServiceRemote(postgres.serverId, postgres.appName);
|
||||
} else {
|
||||
@@ -178,23 +160,22 @@ export const postgresRouter = createTRPCRouter({
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "service",
|
||||
resourceId: postgres.postgresId,
|
||||
resourceName: postgres.appName,
|
||||
});
|
||||
return postgres;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.input(apiSaveExternalPortPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this external port",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.externalPort) {
|
||||
const portCheck = await checkPortInUse(
|
||||
input.externalPort,
|
||||
@@ -212,21 +193,27 @@ export const postgresRouter = createTRPCRouter({
|
||||
externalPort: input.externalPort,
|
||||
});
|
||||
await deployPostgres(input.postgresId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: postgres.postgresId,
|
||||
resourceName: postgres.appName,
|
||||
});
|
||||
return postgres;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Postgres",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "service",
|
||||
resourceId: postgres.postgresId,
|
||||
resourceName: postgres.appName,
|
||||
});
|
||||
return deployPostgres(input.postgresId);
|
||||
}),
|
||||
|
||||
@@ -241,17 +228,9 @@ export const postgresRouter = createTRPCRouter({
|
||||
})
|
||||
.input(apiDeployPostgres)
|
||||
.subscription(async function* ({ input, ctx, signal }) {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Postgres",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
@@ -276,32 +255,25 @@ export const postgresRouter = createTRPCRouter({
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangePostgresStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to change this Postgres status",
|
||||
});
|
||||
}
|
||||
await updatePostgresById(input.postgresId, {
|
||||
applicationStatus: input.applicationStatus,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: postgres.postgresId,
|
||||
resourceName: postgres.appName,
|
||||
});
|
||||
return postgres;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.postgresId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"delete",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.postgresId, "delete");
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
|
||||
if (
|
||||
@@ -314,6 +286,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "service",
|
||||
resourceId: postgres.postgresId,
|
||||
resourceName: postgres.appName,
|
||||
});
|
||||
const backups = await findBackupsByDbId(input.postgresId, "postgres");
|
||||
|
||||
const cleanupOperations = [
|
||||
@@ -333,16 +311,9 @@ export const postgresRouter = createTRPCRouter({
|
||||
saveEnvironment: protectedProcedure
|
||||
.input(apiSaveEnvironmentVariablesPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
envVars: ["write"],
|
||||
});
|
||||
const service = await updatePostgresById(input.postgresId, {
|
||||
env: input.env,
|
||||
});
|
||||
@@ -354,21 +325,20 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: input.postgresId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this Postgres",
|
||||
});
|
||||
}
|
||||
if (postgres.serverId) {
|
||||
await stopServiceRemote(postgres.serverId, postgres.appName);
|
||||
} else {
|
||||
@@ -386,22 +356,21 @@ export const postgresRouter = createTRPCRouter({
|
||||
await updatePostgresById(input.postgresId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "reload",
|
||||
resourceType: "service",
|
||||
resourceId: postgres.postgresId,
|
||||
resourceName: postgres.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdatePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { postgresId, ...rest } = input;
|
||||
const postgres = await findPostgresById(postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this Postgres",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, postgresId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
const service = await updatePostgresById(postgresId, {
|
||||
...rest,
|
||||
@@ -414,6 +383,12 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: postgresId,
|
||||
resourceName: service.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
@@ -424,31 +399,10 @@ export const postgresRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this postgres",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
const targetEnvironment = await findEnvironmentById(
|
||||
input.targetEnvironmentId,
|
||||
);
|
||||
if (
|
||||
targetEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Update the postgres's projectId
|
||||
const updatedPostgres = await db
|
||||
.update(postgresTable)
|
||||
.set({
|
||||
@@ -465,24 +419,28 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "move",
|
||||
resourceType: "service",
|
||||
resourceId: updatedPostgres.postgresId,
|
||||
resourceName: updatedPostgres.appName,
|
||||
});
|
||||
return updatedPostgres;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.input(apiRebuildPostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const postgres = await findPostgresById(input.postgresId);
|
||||
if (
|
||||
postgres.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to rebuild this Postgres database",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.postgresId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
await rebuildDatabase(postgres.postgresId, "postgres");
|
||||
await rebuildDatabase(input.postgresId, "postgres");
|
||||
|
||||
await audit(ctx, {
|
||||
action: "rebuild",
|
||||
resourceType: "service",
|
||||
resourceId: input.postgresId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
@@ -538,19 +496,18 @@ export const postgresRouter = createTRPCRouter({
|
||||
),
|
||||
);
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${postgresTable.postgresId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const { accessedServices } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${postgresTable.postgresId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
|
||||
const where = and(...baseConditions);
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
|
||||
@@ -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,7 +1,4 @@
|
||||
import {
|
||||
addNewEnvironment,
|
||||
addNewProject,
|
||||
checkProjectAccess,
|
||||
createApplication,
|
||||
createBackup,
|
||||
createCompose,
|
||||
@@ -22,7 +19,6 @@ import {
|
||||
findComposeById,
|
||||
findEnvironmentById,
|
||||
findMariadbById,
|
||||
findMemberById,
|
||||
findMongoById,
|
||||
findMySqlById,
|
||||
findPostgresById,
|
||||
@@ -32,15 +28,23 @@ import {
|
||||
IS_CLOUD,
|
||||
updateProjectById,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewEnvironment,
|
||||
addNewProject,
|
||||
checkPermission,
|
||||
checkProjectAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import {
|
||||
apiCreateProject,
|
||||
@@ -63,13 +67,7 @@ export const projectRouter = createTRPCRouter({
|
||||
.input(apiCreateProject)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"create",
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
await checkProjectAccess(ctx, "create");
|
||||
|
||||
const admin = await findUserById(ctx.user.ownerId);
|
||||
|
||||
@@ -84,20 +82,16 @@ export const projectRouter = createTRPCRouter({
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewProject(
|
||||
ctx.user.id,
|
||||
project.project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await addNewProject(ctx, project.project.projectId);
|
||||
|
||||
await addNewEnvironment(
|
||||
ctx.user.id,
|
||||
project?.environment?.environmentId || "",
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
await addNewEnvironment(ctx, project?.environment?.environmentId || "");
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "project",
|
||||
resourceId: project.project.projectId,
|
||||
resourceName: project.project.name,
|
||||
});
|
||||
return project;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -111,18 +105,18 @@ export const projectRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneProject)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedServices, accessedProjects } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"access",
|
||||
ctx.session.activeOrganizationId,
|
||||
input.projectId,
|
||||
);
|
||||
if (!accessedProjects.includes(input.projectId)) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
|
||||
const project = await db.query.projects.findFirst({
|
||||
where: and(
|
||||
@@ -189,15 +183,14 @@ export const projectRouter = createTRPCRouter({
|
||||
return project;
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedProjects, accessedEnvironments, accessedServices } =
|
||||
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
|
||||
await findMemberByUserId(ctx.user.id, ctx.session.activeOrganizationId);
|
||||
|
||||
if (accessedProjects.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Build environment filter
|
||||
const environmentFilter =
|
||||
accessedEnvironments.length === 0
|
||||
? sql`false`
|
||||
@@ -348,105 +341,106 @@ export const projectRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
|
||||
/** All projects with full environments and services for the admin permissions UI. Admin only. */
|
||||
allForPermissions: adminProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.projects.findMany({
|
||||
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: desc(projects.createdAt),
|
||||
columns: {
|
||||
projectId: true,
|
||||
name: true,
|
||||
},
|
||||
with: {
|
||||
environments: {
|
||||
columns: {
|
||||
environmentId: true,
|
||||
name: true,
|
||||
isDefault: true,
|
||||
},
|
||||
with: {
|
||||
applications: {
|
||||
columns: {
|
||||
applicationId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
allForPermissions: withPermission("member", "update").query(
|
||||
async ({ ctx }) => {
|
||||
return await db.query.projects.findMany({
|
||||
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: desc(projects.createdAt),
|
||||
columns: {
|
||||
projectId: true,
|
||||
name: true,
|
||||
},
|
||||
with: {
|
||||
environments: {
|
||||
columns: {
|
||||
environmentId: true,
|
||||
name: true,
|
||||
isDefault: true,
|
||||
},
|
||||
mariadb: {
|
||||
columns: {
|
||||
mariadbId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
with: {
|
||||
applications: {
|
||||
columns: {
|
||||
applicationId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
columns: {
|
||||
postgresId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
mariadb: {
|
||||
columns: {
|
||||
mariadbId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
mysql: {
|
||||
columns: {
|
||||
mysqlId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
postgres: {
|
||||
columns: {
|
||||
postgresId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
mongo: {
|
||||
columns: {
|
||||
mongoId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
mysql: {
|
||||
columns: {
|
||||
mysqlId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
columns: {
|
||||
redisId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
mongo: {
|
||||
columns: {
|
||||
mongoId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
compose: {
|
||||
columns: {
|
||||
composeId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
composeStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
redis: {
|
||||
columns: {
|
||||
redisId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
compose: {
|
||||
columns: {
|
||||
composeId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
composeStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
search: protectedProcedure
|
||||
.input(
|
||||
@@ -482,8 +476,8 @@ export const projectRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedProjects } = await findMemberById(
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedProjects } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
@@ -529,13 +523,6 @@ export const projectRouter = createTRPCRouter({
|
||||
.input(apiRemoveProject)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"delete",
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
const currentProject = await findProjectById(input.projectId);
|
||||
if (
|
||||
currentProject.organizationId !== ctx.session.activeOrganizationId
|
||||
@@ -545,8 +532,15 @@ export const projectRouter = createTRPCRouter({
|
||||
message: "You are not authorized to delete this project",
|
||||
});
|
||||
}
|
||||
await checkProjectAccess(ctx, "delete", input.projectId);
|
||||
const deletedProject = await deleteProject(input.projectId);
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "project",
|
||||
resourceId: currentProject.projectId,
|
||||
resourceName: currentProject.name,
|
||||
});
|
||||
return deletedProject;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -565,10 +559,36 @@ export const projectRouter = createTRPCRouter({
|
||||
message: "You are not authorized to update this project",
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
const { accessedProjects } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (!accessedProjects.includes(input.projectId)) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (input.env !== undefined) {
|
||||
await checkPermission(ctx, { projectEnvVars: ["write"] });
|
||||
}
|
||||
|
||||
const project = await updateProjectById(input.projectId, {
|
||||
...input,
|
||||
});
|
||||
|
||||
if (project) {
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "project",
|
||||
resourceId: input.projectId,
|
||||
resourceName: project.name,
|
||||
});
|
||||
}
|
||||
return project;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -602,15 +622,8 @@ export const projectRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"create",
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}
|
||||
await checkProjectAccess(ctx, "create");
|
||||
|
||||
// Get source project
|
||||
const sourceEnvironment = input.duplicateInSameProject
|
||||
? await findEnvironmentById(input.sourceEnvironmentId)
|
||||
: null;
|
||||
@@ -626,7 +639,24 @@ export const projectRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Create new project or use existing one
|
||||
if (
|
||||
input.duplicateInSameProject &&
|
||||
sourceEnvironment &&
|
||||
ctx.user.role !== "owner" &&
|
||||
ctx.user.role !== "admin"
|
||||
) {
|
||||
const { accessedProjects } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (!accessedProjects.includes(sourceEnvironment.project.projectId)) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const targetProject = input.duplicateInSameProject
|
||||
? sourceEnvironment
|
||||
: await createProject(
|
||||
@@ -643,7 +673,6 @@ export const projectRouter = createTRPCRouter({
|
||||
if (input.includeServices) {
|
||||
const servicesToDuplicate = input.selectedServices || [];
|
||||
|
||||
// Helper function to duplicate a service
|
||||
const duplicateService = async (id: string, type: string) => {
|
||||
switch (type) {
|
||||
case "application": {
|
||||
@@ -947,20 +976,22 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
};
|
||||
|
||||
// Duplicate selected services
|
||||
for (const service of servicesToDuplicate) {
|
||||
await duplicateService(service.id, service.type);
|
||||
}
|
||||
}
|
||||
|
||||
if (!input.duplicateInSameProject && ctx.user.role === "member") {
|
||||
await addNewProject(
|
||||
ctx.user.id,
|
||||
targetProject?.projectId || "",
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (!input.duplicateInSameProject) {
|
||||
await addNewProject(ctx, targetProject?.projectId || "");
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "project",
|
||||
resourceId: targetProject?.projectId || "",
|
||||
resourceName: input.name,
|
||||
metadata: { duplicatedFrom: input.sourceEnvironmentId },
|
||||
});
|
||||
return targetProject;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
|
||||
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,12 +1,9 @@
|
||||
import {
|
||||
addNewService,
|
||||
checkPortInUse,
|
||||
checkServiceAccess,
|
||||
createMount,
|
||||
createRedis,
|
||||
deployRedis,
|
||||
findEnvironmentById,
|
||||
findMemberById,
|
||||
findProjectById,
|
||||
findRedisById,
|
||||
IS_CLOUD,
|
||||
@@ -19,10 +16,17 @@ import {
|
||||
stopServiceRemote,
|
||||
updateRedisById,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
apiChangeRedisStatus,
|
||||
@@ -42,18 +46,10 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(apiCreateRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// Get project from environment
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"create",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, project.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -71,13 +67,7 @@ export const redisRouter = createTRPCRouter({
|
||||
const newRedis = await createRedis({
|
||||
...input,
|
||||
});
|
||||
if (ctx.user.role === "member") {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newRedis.redisId,
|
||||
project.organizationId,
|
||||
);
|
||||
}
|
||||
await addNewService(ctx, newRedis.redisId);
|
||||
|
||||
await createMount({
|
||||
serviceId: newRedis.redisId,
|
||||
@@ -87,6 +77,12 @@ export const redisRouter = createTRPCRouter({
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "service",
|
||||
resourceId: newRedis.redisId,
|
||||
resourceName: newRedis.appName,
|
||||
});
|
||||
return newRedis;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -95,14 +91,7 @@ export const redisRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneRedis)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.redisId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.redisId, "read");
|
||||
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
@@ -120,16 +109,10 @@ export const redisRouter = createTRPCRouter({
|
||||
start: protectedProcedure
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to start this Redis",
|
||||
});
|
||||
}
|
||||
|
||||
if (redis.serverId) {
|
||||
await startServiceRemote(redis.serverId, redis.appName);
|
||||
@@ -140,21 +123,21 @@ export const redisRouter = createTRPCRouter({
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "service",
|
||||
resourceId: redis.redisId,
|
||||
resourceName: redis.appName,
|
||||
});
|
||||
return redis;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to reload this Redis",
|
||||
});
|
||||
}
|
||||
if (redis.serverId) {
|
||||
await stopServiceRemote(redis.serverId, redis.appName);
|
||||
} else {
|
||||
@@ -172,22 +155,22 @@ export const redisRouter = createTRPCRouter({
|
||||
await updateRedisById(input.redisId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "reload",
|
||||
resourceType: "service",
|
||||
resourceId: redis.redisId,
|
||||
resourceName: redis.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to stop this Redis",
|
||||
});
|
||||
}
|
||||
if (redis.serverId) {
|
||||
await stopServiceRemote(redis.serverId, redis.appName);
|
||||
} else {
|
||||
@@ -197,21 +180,21 @@ export const redisRouter = createTRPCRouter({
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "service",
|
||||
resourceId: redis.redisId,
|
||||
resourceName: redis.appName,
|
||||
});
|
||||
return redis;
|
||||
}),
|
||||
saveExternalPort: protectedProcedure
|
||||
.input(apiSaveExternalPortRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this external port",
|
||||
});
|
||||
}
|
||||
|
||||
if (input.externalPort) {
|
||||
const portCheck = await checkPortInUse(
|
||||
@@ -230,21 +213,27 @@ export const redisRouter = createTRPCRouter({
|
||||
externalPort: input.externalPort,
|
||||
});
|
||||
await deployRedis(input.redisId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: redis.redisId,
|
||||
resourceName: redis.appName,
|
||||
});
|
||||
return redis;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Redis",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "service",
|
||||
resourceId: redis.redisId,
|
||||
resourceName: redis.appName,
|
||||
});
|
||||
return deployRedis(input.redisId);
|
||||
}),
|
||||
deployWithLogs: protectedProcedure
|
||||
@@ -258,16 +247,9 @@ export const redisRouter = createTRPCRouter({
|
||||
})
|
||||
.input(apiDeployRedis)
|
||||
.subscription(async function* ({ input, ctx, signal }) {
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to deploy this Redis",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
|
||||
@@ -290,32 +272,25 @@ export const redisRouter = createTRPCRouter({
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeRedisStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const mongo = await findRedisById(input.redisId);
|
||||
if (
|
||||
mongo.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to change this Redis status",
|
||||
});
|
||||
}
|
||||
await updateRedisById(input.redisId, {
|
||||
applicationStatus: input.applicationStatus,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: mongo.redisId,
|
||||
resourceName: mongo.appName,
|
||||
});
|
||||
return mongo;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.redisId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"delete",
|
||||
);
|
||||
}
|
||||
await checkServiceAccess(ctx, input.redisId, "delete");
|
||||
|
||||
const redis = await findRedisById(input.redisId);
|
||||
|
||||
@@ -328,6 +303,12 @@ export const redisRouter = createTRPCRouter({
|
||||
message: "You are not authorized to delete this Redis",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "service",
|
||||
resourceId: redis.redisId,
|
||||
resourceName: redis.appName,
|
||||
});
|
||||
const cleanupOperations = [
|
||||
async () => await removeService(redis?.appName, redis.serverId),
|
||||
async () => await removeRedisById(input.redisId),
|
||||
@@ -344,16 +325,9 @@ export const redisRouter = createTRPCRouter({
|
||||
saveEnvironment: protectedProcedure
|
||||
.input(apiSaveEnvironmentVariablesRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to save this environment",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
envVars: ["write"],
|
||||
});
|
||||
const updatedRedis = await updateRedisById(input.redisId, {
|
||||
env: input.env,
|
||||
});
|
||||
@@ -365,12 +339,20 @@ export const redisRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: input.redisId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateRedis)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { redisId, ...rest } = input;
|
||||
await checkServicePermissionAndAccess(ctx, redisId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const redis = await updateRedisById(redisId, {
|
||||
...rest,
|
||||
});
|
||||
@@ -382,6 +364,12 @@ export const redisRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: redisId,
|
||||
resourceName: redis.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
@@ -392,31 +380,10 @@ export const redisRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move this redis",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
const targetEnvironment = await findEnvironmentById(
|
||||
input.targetEnvironmentId,
|
||||
);
|
||||
if (
|
||||
targetEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to move to this environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Update the redis's projectId
|
||||
const updatedRedis = await db
|
||||
.update(redisTable)
|
||||
.set({
|
||||
@@ -433,23 +400,27 @@ export const redisRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "move",
|
||||
resourceType: "service",
|
||||
resourceId: updatedRedis.redisId,
|
||||
resourceName: updatedRedis.appName,
|
||||
});
|
||||
return updatedRedis;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.input(apiRebuildRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const redis = await findRedisById(input.redisId);
|
||||
if (
|
||||
redis.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to rebuild this Redis database",
|
||||
});
|
||||
}
|
||||
await checkServicePermissionAndAccess(ctx, input.redisId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
await rebuildDatabase(redis.redisId, "redis");
|
||||
await rebuildDatabase(input.redisId, "redis");
|
||||
await audit(ctx, {
|
||||
action: "rebuild",
|
||||
resourceType: "service",
|
||||
resourceId: input.redisId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
@@ -498,19 +469,18 @@ export const redisRouter = createTRPCRouter({
|
||||
ilike(redisTable.description ?? "", `%${input.description.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${redisTable.redisId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const { accessedServices } = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${redisTable.redisId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
|
||||
const where = and(...baseConditions);
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
|
||||
@@ -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 {
|
||||
@@ -245,7 +262,7 @@ export const serverRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
security: protectedProcedure
|
||||
security: withPermission("server", "read")
|
||||
.input(apiFindOneServer)
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -295,7 +312,7 @@ export const serverRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
setupMonitoring: protectedProcedure
|
||||
setupMonitoring: withPermission("server", "create")
|
||||
.input(apiUpdateServerMonitoring)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -332,22 +349,21 @@ export const serverRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
const currentServer = await setupMonitoring(input.serverId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "server",
|
||||
resourceId: input.serverId,
|
||||
resourceName: server.name,
|
||||
});
|
||||
return currentServer;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
remove: withPermission("server", "delete")
|
||||
.input(apiRemoveServer)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this server",
|
||||
});
|
||||
}
|
||||
const activeServers = await haveActiveServices(input.serverId);
|
||||
|
||||
if (activeServers) {
|
||||
@@ -357,6 +373,12 @@ export const serverRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const currentServer = await findServerById(input.serverId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "server",
|
||||
resourceId: currentServer.serverId,
|
||||
resourceName: currentServer.name,
|
||||
});
|
||||
await removeDeploymentsByServerId(currentServer);
|
||||
await deleteServer(input.serverId);
|
||||
|
||||
@@ -371,7 +393,7 @@ export const serverRouter = createTRPCRouter({
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
update: protectedProcedure
|
||||
update: withPermission("server", "create")
|
||||
.input(apiUpdateServer)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -393,6 +415,12 @@ export const serverRouter = createTRPCRouter({
|
||||
...input,
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "server",
|
||||
resourceId: input.serverId,
|
||||
resourceName: server.name,
|
||||
});
|
||||
return currentServer;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -414,7 +442,7 @@ export const serverRouter = createTRPCRouter({
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
};
|
||||
}),
|
||||
getServerMetrics: protectedProcedure
|
||||
getServerMetrics: withPermission("monitoring", "read")
|
||||
.input(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
CLEANUP_CRON_JOB,
|
||||
canAccessToTraefikFiles,
|
||||
checkGPUStatus,
|
||||
checkPortInUse,
|
||||
checkPostgresHealth,
|
||||
checkRedisHealth,
|
||||
checkTraefikHealth,
|
||||
cleanupAll,
|
||||
cleanupAllBackground,
|
||||
cleanupBuilders,
|
||||
@@ -46,12 +48,14 @@ import {
|
||||
writeTraefikSetup,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { checkPermission } from "@dokploy/server/services/permission";
|
||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiAssignDomain,
|
||||
apiEnableDashboard,
|
||||
@@ -84,14 +88,19 @@ export const settingsRouter = createTRPCRouter({
|
||||
const settings = await getWebServerSettings();
|
||||
return settings;
|
||||
}),
|
||||
reloadServer: adminProcedure.mutation(async () => {
|
||||
reloadServer: adminProcedure.mutation(async ({ ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
await reloadDockerResource("dokploy", undefined, packageInfo.version);
|
||||
await audit(ctx, {
|
||||
action: "reload",
|
||||
resourceType: "settings",
|
||||
resourceName: "dokploy",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
cleanRedis: adminProcedure.mutation(async () => {
|
||||
cleanRedis: adminProcedure.mutation(async ({ ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
@@ -107,36 +116,56 @@ export const settingsRouter = createTRPCRouter({
|
||||
const redisContainerId = containerId.trim();
|
||||
|
||||
await execAsync(`docker exec -i ${redisContainerId} redis-cli flushall`);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "clean-redis",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
reloadRedis: adminProcedure.mutation(async () => {
|
||||
reloadRedis: adminProcedure.mutation(async ({ ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
await reloadDockerResource("dokploy-redis");
|
||||
|
||||
await audit(ctx, {
|
||||
action: "reload",
|
||||
resourceType: "settings",
|
||||
resourceName: "dokploy-redis",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
cleanAllDeploymentQueue: adminProcedure.mutation(async () => {
|
||||
cleanAllDeploymentQueue: adminProcedure.mutation(async ({ ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
return cleanAllDeploymentQueue();
|
||||
const result = cleanAllDeploymentQueue();
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "clean-deployment-queue",
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
reloadTraefik: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
// Run in background so the request returns immediately; avoids proxy timeouts.
|
||||
void reloadDockerResource("dokploy-traefik", input?.serverId).catch(
|
||||
(err) => {
|
||||
console.error("reloadTraefik background:", err);
|
||||
},
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "reload",
|
||||
resourceType: "settings",
|
||||
resourceName: "dokploy-traefik",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
toggleDashboard: adminProcedure
|
||||
.input(apiEnableDashboard)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const ports = await readPorts("dokploy-traefik", input.serverId);
|
||||
const env = await readEnvironmentVariables(
|
||||
"dokploy-traefik",
|
||||
@@ -175,70 +204,112 @@ export const settingsRouter = createTRPCRouter({
|
||||
}).catch((err) => {
|
||||
console.error("toggleDashboard background writeTraefikSetup:", err);
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "toggle-dashboard",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
cleanUnusedImages: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await cleanupImages(input?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "settings",
|
||||
resourceName: "clean-unused-images",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
cleanUnusedVolumes: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await cleanupVolumes(input?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "settings",
|
||||
resourceName: "clean-unused-volumes",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
cleanStoppedContainers: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await cleanupContainers(input?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "settings",
|
||||
resourceName: "clean-stopped-containers",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
cleanDockerBuilder: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await cleanupBuilders(input?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "settings",
|
||||
resourceName: "clean-docker-builder",
|
||||
});
|
||||
}),
|
||||
cleanDockerPrune: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await cleanupSystem(input?.serverId);
|
||||
await cleanupBuilders(input?.serverId);
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "settings",
|
||||
resourceName: "clean-docker-prune",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
cleanAll: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
// Execute cleanup in background and return immediately to avoid gateway timeouts
|
||||
const result = await cleanupAllBackground(input?.serverId);
|
||||
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "settings",
|
||||
resourceName: "clean-all",
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
cleanMonitoring: adminProcedure.mutation(async () => {
|
||||
cleanMonitoring: adminProcedure.mutation(async ({ ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
const { MONITORING_PATH } = paths();
|
||||
await recreateDirectory(MONITORING_PATH);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "settings",
|
||||
resourceName: "clean-monitoring",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
saveSSHPrivateKey: adminProcedure
|
||||
.input(apiSaveSSHKey)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
await updateWebServerSettings({
|
||||
sshPrivateKey: input.sshPrivateKey,
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "ssh-private-key",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
assignDomainServer: adminProcedure
|
||||
.input(apiAssignDomain)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
@@ -261,15 +332,25 @@ export const settingsRouter = createTRPCRouter({
|
||||
updateLetsEncryptEmail(input.letsEncryptEmail);
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "assign-domain-server",
|
||||
});
|
||||
return settings;
|
||||
}),
|
||||
cleanSSHPrivateKey: adminProcedure.mutation(async () => {
|
||||
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
await updateWebServerSettings({
|
||||
sshPrivateKey: null,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "settings",
|
||||
resourceName: "ssh-private-key",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
updateDockerCleanup: adminProcedure
|
||||
@@ -349,6 +430,11 @@ export const settingsRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "docker-cleanup",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
|
||||
@@ -362,11 +448,16 @@ export const settingsRouter = createTRPCRouter({
|
||||
|
||||
updateTraefikConfig: adminProcedure
|
||||
.input(apiTraefikConfig)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
writeMainConfig(input.traefikConfig);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "traefik-config",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
|
||||
@@ -379,11 +470,16 @@ export const settingsRouter = createTRPCRouter({
|
||||
}),
|
||||
updateWebServerTraefikConfig: adminProcedure
|
||||
.input(apiTraefikConfig)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
writeConfig("dokploy", input.traefikConfig);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "web-server-traefik-config",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
|
||||
@@ -397,11 +493,16 @@ export const settingsRouter = createTRPCRouter({
|
||||
|
||||
updateMiddlewareTraefikConfig: adminProcedure
|
||||
.input(apiTraefikConfig)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
writeConfig("middlewares", input.traefikConfig);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "middleware-traefik-config",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
getUpdateData: protectedProcedure.mutation(async () => {
|
||||
@@ -411,7 +512,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
|
||||
return await getUpdateData(packageInfo.version);
|
||||
}),
|
||||
updateServer: adminProcedure.mutation(async () => {
|
||||
updateServer: adminProcedure.mutation(async ({ ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
@@ -426,6 +527,11 @@ export const settingsRouter = createTRPCRouter({
|
||||
`dokploy/dokploy:${data.latestVersion}`,
|
||||
"dokploy",
|
||||
]);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "dokploy-version",
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -441,16 +547,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
.input(apiServerSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
const canAccess = await canAccessToTraefikFiles(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
if (!canAccess) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
}
|
||||
await checkPermission(ctx, { traefikFiles: ["read"] });
|
||||
const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId);
|
||||
const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId);
|
||||
return result || [];
|
||||
@@ -462,37 +559,24 @@ export const settingsRouter = createTRPCRouter({
|
||||
updateTraefikFile: protectedProcedure
|
||||
.input(apiModifyTraefikConfig)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
const canAccess = await canAccessToTraefikFiles(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
if (!canAccess) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
}
|
||||
await checkPermission(ctx, { traefikFiles: ["write"] });
|
||||
await writeTraefikConfigInPath(
|
||||
input.path,
|
||||
input.traefikConfig,
|
||||
input?.serverId,
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "traefik-file",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
|
||||
readTraefikFile: protectedProcedure
|
||||
.input(apiReadTraefikConfig)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
const canAccess = await canAccessToTraefikFiles(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
if (!canAccess) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
}
|
||||
await checkPermission(ctx, { traefikFiles: ["read"] });
|
||||
|
||||
if (input.serverId) {
|
||||
const server = await findServerById(input.serverId);
|
||||
@@ -517,13 +601,18 @@ export const settingsRouter = createTRPCRouter({
|
||||
serverIp: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
const settings = await updateWebServerSettings({
|
||||
serverIp: input.serverIp,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "server-ip",
|
||||
});
|
||||
return settings;
|
||||
}),
|
||||
|
||||
@@ -610,7 +699,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
|
||||
writeTraefikEnv: adminProcedure
|
||||
.input(z.object({ env: z.string(), serverId: z.string().optional() }))
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const envs = prepareEnvironmentVariables(input.env);
|
||||
const ports = await readPorts("dokploy-traefik", input?.serverId);
|
||||
|
||||
@@ -622,6 +711,11 @@ export const settingsRouter = createTRPCRouter({
|
||||
}).catch((err) => {
|
||||
console.error("writeTraefikEnv background writeTraefikSetup:", err);
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "traefik-env",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
haveTraefikDashboardPortEnabled: adminProcedure
|
||||
@@ -715,7 +809,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
enable: z.boolean(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
@@ -742,7 +836,11 @@ export const settingsRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
writeMainConfig(stringify(currentConfig));
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "toggle-requests",
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
isCloud: publicProcedure.query(async () => {
|
||||
@@ -769,19 +867,41 @@ export const settingsRouter = createTRPCRouter({
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
checkInfrastructureHealth: adminProcedure.query(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return {
|
||||
postgres: { status: "healthy" as const },
|
||||
redis: { status: "healthy" as const },
|
||||
traefik: { status: "healthy" as const },
|
||||
};
|
||||
}
|
||||
|
||||
const [postgres, redis, traefik] = await Promise.all([
|
||||
checkPostgresHealth(),
|
||||
checkRedisHealth(),
|
||||
checkTraefikHealth(),
|
||||
]);
|
||||
|
||||
return { postgres, redis, traefik };
|
||||
}),
|
||||
setupGPU: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new Error("Select a server to enable the GPU Setup");
|
||||
}
|
||||
|
||||
try {
|
||||
await setupGPUSupport(input.serverId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "setup-gpu",
|
||||
});
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("GPU Setup Error:", error);
|
||||
@@ -835,7 +955,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
@@ -873,6 +993,11 @@ export const settingsRouter = createTRPCRouter({
|
||||
err,
|
||||
);
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "traefik-ports",
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -897,14 +1022,22 @@ export const settingsRouter = createTRPCRouter({
|
||||
cronExpression: z.string().nullable(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
let result: boolean;
|
||||
if (input.cronExpression) {
|
||||
return startLogCleanup(input.cronExpression);
|
||||
result = await startLogCleanup(input.cronExpression);
|
||||
} else {
|
||||
result = await stopLogCleanup();
|
||||
}
|
||||
return stopLogCleanup();
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "settings",
|
||||
resourceName: "log-cleanup",
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
getLogCleanupStatus: protectedProcedure.query(async () => {
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -22,15 +22,21 @@ import {
|
||||
invitation,
|
||||
member,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import {
|
||||
hasPermission,
|
||||
resolvePermissions,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { and, asc, eq, gt } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
withPermission,
|
||||
} from "../trpc";
|
||||
|
||||
const apiCreateApiKey = z.object({
|
||||
@@ -51,7 +57,7 @@ const apiCreateApiKey = z.object({
|
||||
});
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
all: adminProcedure.query(async ({ ctx }) => {
|
||||
all: withPermission("member", "read").query(async ({ ctx }) => {
|
||||
return await db.query.member.findMany({
|
||||
where: eq(member.organizationId, ctx.session.activeOrganizationId),
|
||||
with: {
|
||||
@@ -87,21 +93,28 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
// Allow access if:
|
||||
// 1. User is requesting their own information
|
||||
// 2. User has owner role (admin permissions) AND user is in the same organization
|
||||
// 2. User is owner/admin
|
||||
// 3. User has member.update permission (custom roles managing permissions)
|
||||
if (
|
||||
memberResult.userId !== ctx.user.id &&
|
||||
ctx.user.role !== "owner" &&
|
||||
ctx.user.role !== "admin"
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this user",
|
||||
});
|
||||
const canUpdate = await hasPermission(ctx, { member: ["update"] });
|
||||
if (!canUpdate) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this user",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return memberResult;
|
||||
}),
|
||||
session: protectedProcedure.query(async ({ ctx }) => {
|
||||
session: publicProcedure.query(async ({ ctx }) => {
|
||||
if (!ctx.user || !ctx.session || !ctx.session.activeOrganizationId) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
user: {
|
||||
id: ctx.user.id,
|
||||
@@ -128,6 +141,9 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return memberResult;
|
||||
}),
|
||||
getPermissions: protectedProcedure.query(async ({ ctx }) => {
|
||||
return resolvePermissions(ctx);
|
||||
}),
|
||||
haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (!IS_CLOUD) {
|
||||
return false;
|
||||
@@ -163,19 +179,21 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return memberResult?.user;
|
||||
}),
|
||||
getServerMetrics: protectedProcedure.query(async ({ ctx }) => {
|
||||
const memberResult = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, ctx.user.id),
|
||||
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
||||
),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
getServerMetrics: withPermission("monitoring", "read").query(
|
||||
async ({ ctx }) => {
|
||||
const memberResult = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, ctx.user.id),
|
||||
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
||||
),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
return memberResult?.user;
|
||||
}),
|
||||
return memberResult?.user;
|
||||
},
|
||||
),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateUser)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -210,7 +228,14 @@ export const userRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
try {
|
||||
return await updateUser(ctx.user.id, input);
|
||||
const result = await updateUser(ctx.user.id, input);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "user",
|
||||
resourceId: ctx.user.id,
|
||||
resourceName: ctx.user.email,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -224,15 +249,17 @@ export const userRouter = createTRPCRouter({
|
||||
.query(async ({ input }) => {
|
||||
return await getUserByToken(input.token);
|
||||
}),
|
||||
getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const settings = await getWebServerSettings();
|
||||
return {
|
||||
serverIp: settings?.serverIp,
|
||||
enabledFeatures: user.enablePaidFeatures,
|
||||
metricsConfig: settings?.metricsConfig,
|
||||
};
|
||||
}),
|
||||
getMetricsToken: withPermission("monitoring", "read").query(
|
||||
async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const settings = await getWebServerSettings();
|
||||
return {
|
||||
serverIp: settings?.serverIp,
|
||||
enabledFeatures: user.enablePaidFeatures,
|
||||
metricsConfig: settings?.metricsConfig,
|
||||
};
|
||||
},
|
||||
),
|
||||
remove: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
@@ -294,9 +321,15 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
return await removeUserById(input.userId);
|
||||
const result = await removeUserById(input.userId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "user",
|
||||
resourceId: input.userId,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
assignPermissions: adminProcedure
|
||||
assignPermissions: withPermission("member", "update")
|
||||
.input(apiAssignPermissions)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
@@ -327,6 +360,12 @@ export const userRouter = createTRPCRouter({
|
||||
),
|
||||
),
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "user",
|
||||
resourceId: input.id,
|
||||
metadata: { permissions: rest },
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -344,7 +383,7 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
|
||||
getContainerMetrics: protectedProcedure
|
||||
getContainerMetrics: withPermission("monitoring", "read")
|
||||
.input(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
@@ -426,7 +465,7 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (apiKeyToDelete.userId !== ctx.user.id) {
|
||||
if (apiKeyToDelete.referenceId !== ctx.user.id) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this API key",
|
||||
@@ -434,6 +473,12 @@ export const userRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
await db.delete(apikey).where(eq(apikey.id, input.apiKeyId));
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "user",
|
||||
resourceId: input.apiKeyId,
|
||||
resourceName: apiKeyToDelete.name || undefined,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -461,6 +506,12 @@ export const userRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const apiKey = await createApiKey(ctx.user.id, input);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "user",
|
||||
resourceId: apiKey.id,
|
||||
resourceName: input.name,
|
||||
});
|
||||
return apiKey;
|
||||
}),
|
||||
|
||||
@@ -505,7 +556,7 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return organizations.length;
|
||||
}),
|
||||
sendInvitation: adminProcedure
|
||||
sendInvitation: withPermission("member", "create")
|
||||
.input(
|
||||
z.object({
|
||||
invitationId: z.string().min(1),
|
||||
@@ -571,6 +622,13 @@ export const userRouter = createTRPCRouter({
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "user",
|
||||
resourceId: input.invitationId,
|
||||
resourceName: currentInvitation?.email || "",
|
||||
metadata: { type: "sendInvitation" },
|
||||
});
|
||||
return inviteLink;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -23,8 +23,10 @@ import { TRPCError } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
|
||||
export const volumeBackupsRouter = createTRPCRouter({
|
||||
list: protectedProcedure
|
||||
@@ -42,7 +44,10 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
]),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||
volumeBackup: ["read"],
|
||||
});
|
||||
return await db.query.volumeBackups.findMany({
|
||||
where: eq(volumeBackups[`${input.volumeBackupType}Id`], input.id),
|
||||
with: {
|
||||
@@ -59,7 +64,20 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
}),
|
||||
create: protectedProcedure
|
||||
.input(createVolumeBackupSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const serviceId =
|
||||
input.applicationId ||
|
||||
input.postgresId ||
|
||||
input.mysqlId ||
|
||||
input.mariadbId ||
|
||||
input.mongoId ||
|
||||
input.redisId ||
|
||||
input.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
volumeBackup: ["create"],
|
||||
});
|
||||
}
|
||||
const newVolumeBackup = await createVolumeBackup(input);
|
||||
|
||||
if (newVolumeBackup?.enabled) {
|
||||
@@ -73,6 +91,11 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
await scheduleVolumeBackup(newVolumeBackup.volumeBackupId);
|
||||
}
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "volumeBackup",
|
||||
resourceId: newVolumeBackup?.volumeBackupId,
|
||||
});
|
||||
return newVolumeBackup;
|
||||
}),
|
||||
one: protectedProcedure
|
||||
@@ -81,8 +104,22 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
volumeBackupId: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await findVolumeBackupById(input.volumeBackupId);
|
||||
.query(async ({ input, ctx }) => {
|
||||
const vb = await findVolumeBackupById(input.volumeBackupId);
|
||||
const serviceId =
|
||||
vb.applicationId ||
|
||||
vb.postgresId ||
|
||||
vb.mysqlId ||
|
||||
vb.mariadbId ||
|
||||
vb.mongoId ||
|
||||
vb.redisId ||
|
||||
vb.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
volumeBackup: ["read"],
|
||||
});
|
||||
}
|
||||
return vb;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(
|
||||
@@ -90,12 +127,46 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
volumeBackupId: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await removeVolumeBackup(input.volumeBackupId);
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const vb = await findVolumeBackupById(input.volumeBackupId);
|
||||
const serviceId =
|
||||
vb.applicationId ||
|
||||
vb.postgresId ||
|
||||
vb.mysqlId ||
|
||||
vb.mariadbId ||
|
||||
vb.mongoId ||
|
||||
vb.redisId ||
|
||||
vb.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
volumeBackup: ["delete"],
|
||||
});
|
||||
}
|
||||
const result = await removeVolumeBackup(input.volumeBackupId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "volumeBackup",
|
||||
resourceId: input.volumeBackupId,
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(updateVolumeBackupSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const existingVb = await findVolumeBackupById(input.volumeBackupId);
|
||||
const serviceId =
|
||||
existingVb.applicationId ||
|
||||
existingVb.postgresId ||
|
||||
existingVb.mysqlId ||
|
||||
existingVb.mariadbId ||
|
||||
existingVb.mongoId ||
|
||||
existingVb.redisId ||
|
||||
existingVb.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
volumeBackup: ["update"],
|
||||
});
|
||||
}
|
||||
const updatedVolumeBackup = await updateVolumeBackup(
|
||||
input.volumeBackupId,
|
||||
input,
|
||||
@@ -130,20 +201,45 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
removeVolumeBackupJob(updatedVolumeBackup.volumeBackupId);
|
||||
}
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "volumeBackup",
|
||||
resourceId: updatedVolumeBackup.volumeBackupId,
|
||||
});
|
||||
return updatedVolumeBackup;
|
||||
}),
|
||||
|
||||
runManually: protectedProcedure
|
||||
.input(z.object({ volumeBackupId: z.string().min(1) }))
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const vb = await findVolumeBackupById(input.volumeBackupId);
|
||||
const serviceId =
|
||||
vb.applicationId ||
|
||||
vb.postgresId ||
|
||||
vb.mysqlId ||
|
||||
vb.mariadbId ||
|
||||
vb.mongoId ||
|
||||
vb.redisId ||
|
||||
vb.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
volumeBackup: ["create"],
|
||||
});
|
||||
}
|
||||
try {
|
||||
return await runVolumeBackup(input.volumeBackupId);
|
||||
const result = await runVolumeBackup(input.volumeBackupId);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "volumeBackup",
|
||||
resourceId: input.volumeBackupId,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
restoreVolumeBackupWithLogs: protectedProcedure
|
||||
restoreVolumeBackupWithLogs: withPermission("volumeBackup", "restore")
|
||||
.meta({
|
||||
openapi: {
|
||||
enabled: false,
|
||||
|
||||
@@ -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