diff --git a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx index 0aa2089cb..033231fc8 100644 --- a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx +++ b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx @@ -63,14 +63,6 @@ export const AdvancedEnvironmentSelector = ({ const [name, setName] = useState(""); const [description, setDescription] = useState(""); - // API mutations - const { data: environment } = api.environment.one.useQuery( - { environmentId: currentEnvironmentId || "" }, - { - enabled: !!currentEnvironmentId, - }, - ); - // Get current user's permissions const { data: currentUser } = api.user.get.useQuery(); @@ -80,6 +72,12 @@ export const AdvancedEnvironmentSelector = ({ currentUser?.role === "admin" || currentUser?.canCreateEnvironments === true; + // Check if user can delete environments + const canDeleteEnvironments = + currentUser?.role === "owner" || + currentUser?.role === "admin" || + currentUser?.canDeleteEnvironments === true; + const haveServices = selectedEnvironment && ((selectedEnvironment?.mariadb?.length || 0) > 0 || @@ -276,17 +274,19 @@ export const AdvancedEnvironmentSelector = ({ - + {canDeleteEnvironments && ( + + )} )} diff --git a/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx b/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx index a9ffcbb5a..fb4d01547 100644 --- a/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx +++ b/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx @@ -161,6 +161,7 @@ const addPermissions = z.object({ canCreateServices: z.boolean().optional().default(false), canDeleteProjects: z.boolean().optional().default(false), canDeleteServices: z.boolean().optional().default(false), + canDeleteEnvironments: z.boolean().optional().default(false), canAccessToTraefikFiles: z.boolean().optional().default(false), canAccessToDocker: z.boolean().optional().default(false), canAccessToAPI: z.boolean().optional().default(false), @@ -196,6 +197,7 @@ export const AddUserPermissions = ({ userId }: Props) => { accessedProjects: [], accessedEnvironments: [], accessedServices: [], + canDeleteEnvironments: false, canCreateProjects: false, canCreateServices: false, canDeleteProjects: false, @@ -216,16 +218,17 @@ export const AddUserPermissions = ({ userId }: Props) => { accessedProjects: data.accessedProjects || [], accessedEnvironments: data.accessedEnvironments || [], accessedServices: data.accessedServices || [], - canCreateProjects: data.canCreateProjects || false, - canCreateServices: data.canCreateServices || false, - canDeleteProjects: data.canDeleteProjects || false, - canDeleteServices: data.canDeleteServices || false, - canAccessToTraefikFiles: data.canAccessToTraefikFiles || false, - canAccessToDocker: data.canAccessToDocker || false, - canAccessToAPI: data.canAccessToAPI || false, - canAccessToSSHKeys: data.canAccessToSSHKeys || false, - canAccessToGitProviders: data.canAccessToGitProviders || false, - canCreateEnvironments: data.canCreateEnvironments || false, + canCreateProjects: data.canCreateProjects, + canCreateServices: data.canCreateServices, + canDeleteProjects: data.canDeleteProjects, + canDeleteServices: data.canDeleteServices, + canDeleteEnvironments: data.canDeleteEnvironments || false, + canAccessToTraefikFiles: data.canAccessToTraefikFiles, + canAccessToDocker: data.canAccessToDocker, + canAccessToAPI: data.canAccessToAPI, + canAccessToSSHKeys: data.canAccessToSSHKeys, + canAccessToGitProviders: data.canAccessToGitProviders, + canCreateEnvironments: data.canCreateEnvironments, }); } }, [form, form.reset, data, isOpen]); @@ -237,6 +240,7 @@ export const AddUserPermissions = ({ userId }: Props) => { canCreateProjects: data.canCreateProjects, canDeleteServices: data.canDeleteServices, canDeleteProjects: data.canDeleteProjects, + canDeleteEnvironments: data.canDeleteEnvironments, canAccessToTraefikFiles: data.canAccessToTraefikFiles, accessedProjects: data.accessedProjects || [], accessedEnvironments: data.accessedEnvironments || [], @@ -379,6 +383,26 @@ export const AddUserPermissions = ({ userId }: Props) => { )} /> + ( + +
+ Delete Environments + + Allow the user to delete environments + +
+ + + +
+ )} + /> { try { - if (ctx.user.role === "member") { - await checkEnvironmentAccess( - ctx.user.id, - input.environmentId, - ctx.session.activeOrganizationId, - "access", - ); - } const environment = await findEnvironmentById(input.environmentId); if ( environment.project.organizationId !== @@ -213,27 +206,33 @@ export const environmentRouter = createTRPCRouter({ }); } - // Check environment access for members - if (ctx.user.role === "member") { - const { accessedEnvironments } = await findMemberById( - ctx.user.id, - ctx.session.activeOrganizationId, - ); + // Check environment deletion permission + await checkEnvironmentDeletionPermission( + ctx.user.id, + environment.projectId, + ctx.session.activeOrganizationId, + ); - if (!accessedEnvironments.includes(environment.environmentId)) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "You are not allowed to delete this environment", - }); - } + // Additional check for environment access for members + if (ctx.user.role === "member") { + await checkEnvironmentAccess( + ctx.user.id, + input.environmentId, + ctx.session.activeOrganizationId, + "access", + ); } const deletedEnvironment = await deleteEnvironment(input.environmentId); return deletedEnvironment; } catch (error) { + if (error instanceof TRPCError) { + throw error; + } throw new TRPCError({ code: "BAD_REQUEST", message: `Error deleting the environment: ${error instanceof Error ? error.message : error}`, + cause: error, }); } }), diff --git a/packages/server/src/db/schema/account.ts b/packages/server/src/db/schema/account.ts index be2332500..f3d70e680 100644 --- a/packages/server/src/db/schema/account.ts +++ b/packages/server/src/db/schema/account.ts @@ -108,6 +108,9 @@ export const member = pgTable("member", { canAccessToTraefikFiles: boolean("canAccessToTraefikFiles") .notNull() .default(false), + canDeleteEnvironments: boolean("canDeleteEnvironments") + .notNull() + .default(false), canCreateEnvironments: boolean("canCreateEnvironments") .notNull() .default(false), diff --git a/packages/server/src/db/schema/user.ts b/packages/server/src/db/schema/user.ts index 920138c16..9b4ab78e9 100644 --- a/packages/server/src/db/schema/user.ts +++ b/packages/server/src/db/schema/user.ts @@ -186,6 +186,7 @@ export const apiAssignPermissions = createSchema canAccessToAPI: z.boolean().optional(), canAccessToSSHKeys: z.boolean().optional(), canAccessToGitProviders: z.boolean().optional(), + canDeleteEnvironments: z.boolean().optional(), canCreateEnvironments: z.boolean().optional(), }) .required(); diff --git a/packages/server/src/services/user.ts b/packages/server/src/services/user.ts index 1ea7d4568..8afce6b55 100644 --- a/packages/server/src/services/user.ts +++ b/packages/server/src/services/user.ts @@ -163,6 +163,24 @@ export const canPerformAccessEnvironment = async ( return false; }; +export const canPerformDeleteEnvironment = async ( + userId: string, + projectId: string, + organizationId: string, +) => { + const { accessedProjects, canDeleteEnvironments } = await findMemberById( + userId, + organizationId, + ); + const haveAccessToProject = accessedProjects.includes(projectId); + + if (canDeleteEnvironments && haveAccessToProject) { + return true; + } + + return false; +}; + export const canAccessToTraefikFiles = async ( userId: string, organizationId: string, @@ -240,6 +258,42 @@ export const checkEnvironmentAccess = async ( } }; +export const checkEnvironmentDeletionPermission = async ( + userId: string, + projectId: string, + organizationId: string, +) => { + const member = await findMemberById(userId, organizationId); + + if (!member) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "User not found in organization", + }); + } + + if (member.role === "owner" || member.role === "admin") { + return true; + } + + if (!member.canDeleteEnvironments) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have permission to delete environments", + }); + } + + const hasProjectAccess = member.accessedProjects.includes(projectId); + if (!hasProjectAccess) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have access to this project", + }); + } + + return true; +}; + export const checkProjectAccess = async ( authId: string, action: "create" | "delete" | "access",