diff --git a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx index d6497fd0f..0aa2089cb 100644 --- a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx +++ b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx @@ -71,6 +71,15 @@ export const AdvancedEnvironmentSelector = ({ }, ); + // Get current user's permissions + const { data: currentUser } = api.user.get.useQuery(); + + // Check if user can create environments + const canCreateEnvironments = + currentUser?.role === "owner" || + currentUser?.role === "admin" || + currentUser?.canCreateEnvironments === true; + const haveServices = selectedEnvironment && ((selectedEnvironment?.mariadb?.length || 0) > 0 || @@ -285,13 +294,15 @@ export const AdvancedEnvironmentSelector = ({ })} - setIsCreateDialogOpen(true)} - > - - Create Environment - + {canCreateEnvironments && ( + setIsCreateDialogOpen(true)} + > + + Create Environment + + )} diff --git a/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx b/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx index a918da98f..a9ffcbb5a 100644 --- a/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx +++ b/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx @@ -1,6 +1,6 @@ import type { findEnvironmentById } from "@dokploy/server/index"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; @@ -166,6 +166,7 @@ const addPermissions = z.object({ canAccessToAPI: z.boolean().optional().default(false), canAccessToSSHKeys: z.boolean().optional().default(false), canAccessToGitProviders: z.boolean().optional().default(false), + canCreateEnvironments: z.boolean().optional().default(false), }); type AddPermissions = z.infer; @@ -175,6 +176,7 @@ interface Props { } export const AddUserPermissions = ({ userId }: Props) => { + const [isOpen, setIsOpen] = useState(false); const { data: projects } = api.project.all.useQuery(); const { data, refetch } = api.user.one.useQuery( @@ -192,29 +194,41 @@ export const AddUserPermissions = ({ userId }: Props) => { const form = useForm({ defaultValues: { accessedProjects: [], + accessedEnvironments: [], accessedServices: [], + canCreateProjects: false, + canCreateServices: false, + canDeleteProjects: false, + canDeleteServices: false, + canAccessToTraefikFiles: false, + canAccessToDocker: false, + canAccessToAPI: false, + canAccessToSSHKeys: false, + canAccessToGitProviders: false, + canCreateEnvironments: false, }, resolver: zodResolver(addPermissions), }); useEffect(() => { - if (data) { + if (data && isOpen) { form.reset({ accessedProjects: data.accessedProjects || [], accessedEnvironments: data.accessedEnvironments || [], accessedServices: data.accessedServices || [], - canCreateProjects: data.canCreateProjects, - canCreateServices: data.canCreateServices, - canDeleteProjects: data.canDeleteProjects, - canDeleteServices: data.canDeleteServices, - canAccessToTraefikFiles: data.canAccessToTraefikFiles, - canAccessToDocker: data.canAccessToDocker, - canAccessToAPI: data.canAccessToAPI, - canAccessToSSHKeys: data.canAccessToSSHKeys, - canAccessToGitProviders: data.canAccessToGitProviders, + 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, }); } - }, [form, form.formState.isSubmitSuccessful, form.reset, data]); + }, [form, form.reset, data, isOpen]); const onSubmit = async (data: AddPermissions) => { await mutateAsync({ @@ -231,17 +245,19 @@ export const AddUserPermissions = ({ userId }: Props) => { canAccessToAPI: data.canAccessToAPI, canAccessToSSHKeys: data.canAccessToSSHKeys, canAccessToGitProviders: data.canAccessToGitProviders, + canCreateEnvironments: data.canCreateEnvironments, }) .then(async () => { toast.success("Permissions updated"); refetch(); + setIsOpen(false); }) .catch(() => { toast.error("Error updating the permissions"); }); }; return ( - + { )} /> + ( + + + Create Environments + + Allow the user to create environments + + + + + + + )} + /> { try { - // Check if user has access to the project - // This would typically involve checking project ownership/membership - // For now, we'll use a basic organization check + // Check if user has permission to create environments + await checkEnvironmentCreationPermission( + ctx.user.id, + input.projectId, + ctx.session.activeOrganizationId, + ); if (input.name === "production") { throw new TRPCError({ @@ -76,6 +80,9 @@ export const environmentRouter = createTRPCRouter({ } return environment; } catch (error) { + if (error instanceof TRPCError) { + throw error; + } throw new TRPCError({ code: "BAD_REQUEST", message: `Error creating the environment: ${error instanceof Error ? error.message : error}`, diff --git a/apps/dokploy/server/api/routers/project.ts b/apps/dokploy/server/api/routers/project.ts index 5dd2ee695..9f46d7de3 100644 --- a/apps/dokploy/server/api/routers/project.ts +++ b/apps/dokploy/server/api/routers/project.ts @@ -1,4 +1,5 @@ import { + addNewEnvironment, addNewProject, checkProjectAccess, createApplication, @@ -85,6 +86,12 @@ export const projectRouter = createTRPCRouter({ project.project.projectId, ctx.session.activeOrganizationId, ); + + await addNewEnvironment( + ctx.user.id, + project?.environment?.environmentId || "", + ctx.session.activeOrganizationId, + ); } return project; diff --git a/packages/server/src/db/schema/account.ts b/packages/server/src/db/schema/account.ts index 3eb57b552..be2332500 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), + canCreateEnvironments: boolean("canCreateEnvironments") + .notNull() + .default(false), accessedProjects: text("accesedProjects") .array() .notNull() diff --git a/packages/server/src/db/schema/user.ts b/packages/server/src/db/schema/user.ts index ca92f50e8..920138c16 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(), + canCreateEnvironments: z.boolean().optional(), }) .required(); diff --git a/packages/server/src/services/user.ts b/packages/server/src/services/user.ts index ae03432a1..1ea7d4568 100644 --- a/packages/server/src/services/user.ts +++ b/packages/server/src/services/user.ts @@ -272,6 +272,46 @@ export const checkProjectAccess = async ( } }; +export const checkEnvironmentCreationPermission = async ( + userId: string, + projectId: string, + organizationId: string, +) => { + // Get user's member record + const member = await findMemberById(userId, organizationId); + + if (!member) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "User not found in organization", + }); + } + + // Owners and admins can always create environments + if (member.role === "owner" || member.role === "admin") { + return true; + } + + // Check if user has canCreateEnvironments permission + if (!member.canCreateEnvironments) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have permission to create environments", + }); + } + + // Check if user has access to the project + 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 findMemberById = async ( userId: string, organizationId: string,