mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 04:35:24 +02:00
Merge pull request #2598 from Harikrishnan1367709/separate-permission-for-creating-environments-#2593
feat: Add environment creation permission control-#2593
This commit is contained in:
@@ -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 = ({
|
||||
})}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => setIsCreateDialogOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create Environment
|
||||
</DropdownMenuItem>
|
||||
{canCreateEnvironments && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => setIsCreateDialogOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create Environment
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
|
||||
@@ -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<typeof addPermissions>;
|
||||
@@ -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<AddPermissions>({
|
||||
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 (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
@@ -343,6 +359,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canCreateEnvironments"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Create Environments</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to create environments
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToTraefikFiles"
|
||||
|
||||
1
apps/dokploy/drizzle/0115_serious_black_bird.sql
Normal file
1
apps/dokploy/drizzle/0115_serious_black_bird.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "member" ADD COLUMN "canCreateEnvironments" boolean DEFAULT false NOT NULL;
|
||||
6615
apps/dokploy/drizzle/meta/0115_snapshot.json
Normal file
6615
apps/dokploy/drizzle/meta/0115_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -806,6 +806,13 @@
|
||||
"when": 1759643172958,
|
||||
"tag": "0114_dry_black_tom",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 115,
|
||||
"version": "7",
|
||||
"when": 1759644540829,
|
||||
"tag": "0115_serious_black_bird",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
addNewEnvironment,
|
||||
checkEnvironmentAccess,
|
||||
checkEnvironmentCreationPermission,
|
||||
createEnvironment,
|
||||
deleteEnvironment,
|
||||
duplicateEnvironment,
|
||||
@@ -54,9 +55,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
.input(apiCreateEnvironment)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
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}`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user