mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
feat(permissions): implement role-based access control and refactor user permissions
- Introduced a new Permissions component to manage role-based access across various components. - Updated user role checks to utilize the new permissions structure, replacing direct role comparisons with permission checks. - Refactored multiple components to enhance permission handling, ensuring only authorized users can access specific features. - Removed deprecated add-permissions component and streamlined user permission management. - Enhanced role management in the backend to support the new permissions schema, improving overall security and maintainability.
This commit is contained in:
@@ -47,11 +47,12 @@ import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { HandleProject } from "./handle-project";
|
||||
import { ProjectEnvironment } from "./project-environment";
|
||||
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||
import { Permissions } from "../shared/Permissions";
|
||||
|
||||
export const ShowProjects = () => {
|
||||
const utils = api.useUtils();
|
||||
const { data, isLoading } = api.project.all.useQuery();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { mutateAsync } = api.project.remove.useMutation();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
@@ -83,11 +84,11 @@ export const ShowProjects = () => {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
{(auth?.role === "owner" || auth?.canCreateProjects) && (
|
||||
<Permissions permissions={[PERMISSIONS.PROJECT.CREATE.name]}>
|
||||
<div className="">
|
||||
<HandleProject />
|
||||
</div>
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
|
||||
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
||||
@@ -289,8 +290,11 @@ export const ShowProjects = () => {
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.canDeleteProjects) && (
|
||||
<Permissions
|
||||
permissions={[
|
||||
PERMISSIONS.PROJECT.DELETE.name,
|
||||
]}
|
||||
>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
@@ -356,7 +360,7 @@ export const ShowProjects = () => {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -14,7 +14,7 @@ export const ShowWelcomeDokploy = () => {
|
||||
|
||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||
|
||||
if (!isCloud || data?.role !== "admin") {
|
||||
if (!isCloud || data?.role?.name !== "admin") {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@ export const ShowWelcomeDokploy = () => {
|
||||
!isLoading &&
|
||||
isCloud &&
|
||||
!localStorage.getItem("hasSeenCloudWelcomeModal") &&
|
||||
data?.role === "owner"
|
||||
data?.role?.name === "owner"
|
||||
) {
|
||||
setOpen(true);
|
||||
}
|
||||
}, [isCloud, isLoading]);
|
||||
|
||||
const handleClose = (isOpen: boolean) => {
|
||||
if (data?.role === "owner") {
|
||||
if (data?.role?.name === "owner") {
|
||||
setOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
localStorage.setItem("hasSeenCloudWelcomeModal", "true"); // Establece el flag al cerrar el modal
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Disable2FA } from "./disable-2fa";
|
||||
import { Enable2FA } from "./enable-2fa";
|
||||
|
||||
const profileSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
password: z.string().nullable(),
|
||||
currentPassword: z.string().nullable(),
|
||||
@@ -79,6 +80,7 @@ export const ProfileForm = () => {
|
||||
|
||||
const form = useForm<Profile>({
|
||||
defaultValues: {
|
||||
name: data?.user?.name || "",
|
||||
email: data?.user?.email || "",
|
||||
password: "",
|
||||
image: data?.user?.image || "",
|
||||
@@ -92,6 +94,7 @@ export const ProfileForm = () => {
|
||||
if (data) {
|
||||
form.reset(
|
||||
{
|
||||
name: data?.user?.name || "",
|
||||
email: data?.user?.email || "",
|
||||
password: form.getValues("password") || "",
|
||||
image: data?.user?.image || "",
|
||||
@@ -114,6 +117,7 @@ export const ProfileForm = () => {
|
||||
|
||||
const onSubmit = async (values: Profile) => {
|
||||
await mutateAsync({
|
||||
name: values.name,
|
||||
email: values.email.toLowerCase(),
|
||||
password: values.password || undefined,
|
||||
image: values.image,
|
||||
@@ -124,6 +128,7 @@ export const ProfileForm = () => {
|
||||
await refetch();
|
||||
toast.success("Profile Updated");
|
||||
form.reset({
|
||||
name: values.name,
|
||||
email: values.email,
|
||||
password: "",
|
||||
image: values.image,
|
||||
@@ -167,6 +172,19 @@ export const ProfileForm = () => {
|
||||
className="grid gap-4"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
|
||||
@@ -1,444 +0,0 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { extractServices } from "@/pages/dashboard/project/[projectId]";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const addPermissions = z.object({
|
||||
accessedProjects: z.array(z.string()).optional(),
|
||||
accessedServices: z.array(z.string()).optional(),
|
||||
canCreateProjects: z.boolean().optional().default(false),
|
||||
canCreateServices: z.boolean().optional().default(false),
|
||||
canDeleteProjects: z.boolean().optional().default(false),
|
||||
canDeleteServices: z.boolean().optional().default(false),
|
||||
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
||||
canAccessToDocker: z.boolean().optional().default(false),
|
||||
canAccessToAPI: z.boolean().optional().default(false),
|
||||
canAccessToSSHKeys: z.boolean().optional().default(false),
|
||||
canAccessToGitProviders: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
type AddPermissions = z.infer<typeof addPermissions>;
|
||||
|
||||
interface Props {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export const AddUserPermissions = ({ userId }: Props) => {
|
||||
const { data: projects } = api.project.all.useQuery();
|
||||
|
||||
const { data, refetch } = api.user.one.useQuery(
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
enabled: !!userId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.user.assignPermissions.useMutation();
|
||||
|
||||
const form = useForm<AddPermissions>({
|
||||
defaultValues: {
|
||||
accessedProjects: [],
|
||||
accessedServices: [],
|
||||
},
|
||||
resolver: zodResolver(addPermissions),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
accessedProjects: data.accessedProjects || [],
|
||||
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,
|
||||
});
|
||||
}
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: AddPermissions) => {
|
||||
await mutateAsync({
|
||||
id: userId,
|
||||
canCreateServices: data.canCreateServices,
|
||||
canCreateProjects: data.canCreateProjects,
|
||||
canDeleteServices: data.canDeleteServices,
|
||||
canDeleteProjects: data.canDeleteProjects,
|
||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||
accessedProjects: data.accessedProjects || [],
|
||||
accessedServices: data.accessedServices || [],
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||
canAccessToGitProviders: data.canAccessToGitProviders,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Permissions updated");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating the permissions");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Add Permissions
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Permissions</DialogTitle>
|
||||
<DialogDescription>Add or remove permissions</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-permissions"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canCreateProjects"
|
||||
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 Projects</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to create projects
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canDeleteProjects"
|
||||
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>Delete Projects</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to delete projects
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canCreateServices"
|
||||
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 Services</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to create services
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canDeleteServices"
|
||||
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>Delete Services</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to delete services
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToTraefikFiles"
|
||||
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>Access to Traefik Files</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to access to the Traefik Tab Files
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToDocker"
|
||||
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>Access to Docker</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to access to the Docker Tab
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToAPI"
|
||||
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>Access to API/CLI</FormLabel>
|
||||
<FormDescription>
|
||||
Allow the user to access to the API/CLI
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToSSHKeys"
|
||||
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>Access to SSH Keys</FormLabel>
|
||||
<FormDescription>
|
||||
Allow to users to access to the SSH Keys section
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToGitProviders"
|
||||
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>Access to Git Providers</FormLabel>
|
||||
<FormDescription>
|
||||
Allow to users to access to the Git Providers section
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accessedProjects"
|
||||
render={() => (
|
||||
<FormItem className="md:col-span-2">
|
||||
<div className="mb-4">
|
||||
<FormLabel className="text-base">Projects</FormLabel>
|
||||
<FormDescription>
|
||||
Select the Projects that the user can access
|
||||
</FormDescription>
|
||||
</div>
|
||||
{projects?.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No projects found
|
||||
</p>
|
||||
)}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{projects?.map((item, index) => {
|
||||
const applications = extractServices(item);
|
||||
return (
|
||||
<FormField
|
||||
key={`project-${index}`}
|
||||
control={form.control}
|
||||
name="accessedProjects"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={item.projectId}
|
||||
className="flex flex-col items-start space-x-4 rounded-lg p-4 border"
|
||||
>
|
||||
<div className="flex flex-row gap-4">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(
|
||||
item.projectId,
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([
|
||||
...(field.value || []),
|
||||
item.projectId,
|
||||
])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) =>
|
||||
value !== item.projectId,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="text-sm font-medium text-primary">
|
||||
{item.name}
|
||||
</FormLabel>
|
||||
</div>
|
||||
{applications.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No services found
|
||||
</p>
|
||||
)}
|
||||
{applications?.map((item, index) => (
|
||||
<FormField
|
||||
key={`project-${index}`}
|
||||
control={form.control}
|
||||
name="accessedServices"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={item.id}
|
||||
className="flex flex-row items-start space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(
|
||||
item.id,
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([
|
||||
...(field.value || []),
|
||||
item.id,
|
||||
])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) =>
|
||||
value !== item.id,
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="text-sm text-muted-foreground">
|
||||
{item.name}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-add-permissions"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -30,7 +30,6 @@ import { format } from "date-fns";
|
||||
import { MoreHorizontal, Users } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AddUserPermissions } from "./add-permissions";
|
||||
import { AddUserPermissionsV2 } from "./add-permissions-v2";
|
||||
|
||||
export const ShowUsers = () => {
|
||||
@@ -135,9 +134,6 @@ export const ShowUsers = () => {
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<>
|
||||
<AddUserPermissions
|
||||
userId={member.user.id}
|
||||
/>
|
||||
<AddUserPermissionsV2
|
||||
userId={member.user.id}
|
||||
/>
|
||||
|
||||
28
apps/dokploy/components/dashboard/shared/Permissions.tsx
Normal file
28
apps/dokploy/components/dashboard/shared/Permissions.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { api } from "@/utils/api";
|
||||
import type { PermissionName } from "@dokploy/server/lib/permissions";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface Props {
|
||||
permissions: PermissionName[];
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Permissions = ({ permissions, children }: Props) => {
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const hasPermission = useMemo(() => {
|
||||
if (auth?.role?.name === "owner" || auth?.role?.name === "admin") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return permissions.some((permission) =>
|
||||
auth?.role?.permissions?.includes(permission),
|
||||
);
|
||||
}, [permissions, auth]);
|
||||
|
||||
if (!hasPermission) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -87,6 +87,7 @@ import { Logo } from "../shared/logo";
|
||||
import { Button } from "../ui/button";
|
||||
import { UpdateServerButton } from "./update-server";
|
||||
import { UserNav } from "./user-nav";
|
||||
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||
|
||||
type AuthQueryOutput = inferRouterOutputs<AppRouter>["user"]["get"];
|
||||
|
||||
@@ -152,7 +153,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/schedules",
|
||||
icon: Clock,
|
||||
// Only enabled in non-cloud environments
|
||||
isEnabled: ({ isCloud, auth }) => !isCloud && auth?.role === "owner",
|
||||
isEnabled: ({ isCloud, auth }) =>
|
||||
!isCloud && auth?.role?.name === "owner",
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -162,7 +164,10 @@ const MENU: Menu = {
|
||||
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" || auth?.canAccessToTraefikFiles) &&
|
||||
(auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(
|
||||
PERMISSIONS.TRAEFIK.ACCESS.name,
|
||||
)) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
@@ -173,7 +178,11 @@ const MENU: Menu = {
|
||||
icon: BlocksIcon,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
||||
!!(
|
||||
(auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -182,7 +191,11 @@ const MENU: Menu = {
|
||||
icon: PieChart,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
||||
!!(
|
||||
(auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -191,7 +204,11 @@ const MENU: Menu = {
|
||||
icon: Forward,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
||||
!!(
|
||||
(auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -202,7 +219,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/server",
|
||||
icon: Activity,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(auth?.role?.name === "owner" && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -216,7 +234,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/servers",
|
||||
icon: Server,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -224,7 +242,7 @@ const MENU: Menu = {
|
||||
icon: Users,
|
||||
url: "/dashboard/settings/users",
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -233,14 +251,17 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/ssh-keys",
|
||||
// Only enabled for admins and users with access to SSH keys
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.canAccessToSSHKeys),
|
||||
!!(
|
||||
auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(PERMISSIONS.SSH_KEYS.ACCESS.name)
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "AI",
|
||||
icon: BotIcon,
|
||||
url: "/dashboard/settings/ai",
|
||||
isSingle: true,
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -249,7 +270,12 @@ const MENU: Menu = {
|
||||
icon: GitBranch,
|
||||
// Only enabled for admins and users with access to Git providers
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.canAccessToGitProviders),
|
||||
!!(
|
||||
auth?.role?.name === "owner" ||
|
||||
auth?.role?.permissions?.includes(
|
||||
PERMISSIONS.GIT_PROVIDERS.ACCESS.name,
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -257,7 +283,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/registry",
|
||||
icon: Package,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -265,7 +291,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/destinations",
|
||||
icon: Database,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
},
|
||||
|
||||
{
|
||||
@@ -274,7 +300,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/certificates",
|
||||
icon: ShieldCheck,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -282,7 +308,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/cluster",
|
||||
icon: Boxes,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(auth?.role?.name === "owner" && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -290,7 +317,7 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/notifications",
|
||||
icon: Bell,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) => !!(auth?.role?.name === "owner"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -298,7 +325,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/billing",
|
||||
icon: CreditCard,
|
||||
// Only enabled for admins in cloud environments
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(auth?.role?.name === "owner" && isCloud),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -595,7 +623,7 @@ function SidebarLogo() {
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{(user?.role === "owner" || isCloud) && (
|
||||
{(user?.role?.name === "owner" || isCloud) && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<AddOrganization />
|
||||
@@ -961,7 +989,7 @@ export default function Page({ children }: Props) {
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<SidebarMenu className="flex flex-col gap-2">
|
||||
{!isCloud && auth?.role === "owner" && (
|
||||
{!isCloud && auth?.role?.name === "owner" && (
|
||||
<SidebarMenuItem>
|
||||
<UpdateServerButton />
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -23,6 +23,8 @@ import { ChevronsUpDown } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { ModeToggle } from "../ui/modeToggle";
|
||||
import { SidebarMenuButton } from "../ui/sidebar";
|
||||
import { Permissions } from "../dashboard/shared/Permissions";
|
||||
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||
|
||||
const _AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
||||
|
||||
@@ -98,7 +100,7 @@ export const UserNav = () => {
|
||||
>
|
||||
Monitoring
|
||||
</DropdownMenuItem>
|
||||
{(data?.role === "owner" || data?.canAccessToTraefikFiles) && (
|
||||
<Permissions permissions={[PERMISSIONS.TRAEFIK.ACCESS.name]}>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -107,8 +109,9 @@ export const UserNav = () => {
|
||||
>
|
||||
Traefik
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{(data?.role === "owner" || data?.canAccessToDocker) && (
|
||||
</Permissions>
|
||||
|
||||
<Permissions permissions={[PERMISSIONS.DOCKER.VIEW.name]}>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -119,11 +122,11 @@ export const UserNav = () => {
|
||||
>
|
||||
Docker
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</Permissions>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{data?.role === "owner" && (
|
||||
{data?.role?.name === "owner" && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -136,7 +139,7 @@ export const UserNav = () => {
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
{isCloud && data?.role === "owner" && (
|
||||
{isCloud && data?.role?.name === "owner" && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -154,9 +157,6 @@ export const UserNav = () => {
|
||||
await authClient.signOut().then(() => {
|
||||
router.push("/");
|
||||
});
|
||||
// await mutateAsync().then(() => {
|
||||
// router.push("/");
|
||||
// });
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { PERMISSIONS } from "@dokploy/server/lib/permissions";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
@@ -53,12 +54,13 @@ export async function getServerSideProps(
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
|
||||
if (user.role?.name === "member" || user?.role?.) {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!userR?.canAccessToDocker) {
|
||||
if (!userR?.role?.permissions?.includes(PERMISSIONS.DOCKER.VIEW.name)) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -94,6 +94,7 @@ import { useRouter } from "next/router";
|
||||
import { type ReactElement, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
|
||||
export type Services = {
|
||||
appName: string;
|
||||
@@ -221,7 +222,6 @@ const Project = (
|
||||
) => {
|
||||
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
||||
const { projectId } = props;
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const [sortBy, setSortBy] = useState<string>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return localStorage.getItem("servicesSort") || "createdAt-desc";
|
||||
@@ -736,30 +736,27 @@ const Project = (
|
||||
Stop
|
||||
</Button>
|
||||
</DialogAction>
|
||||
{(auth?.role === "owner" ||
|
||||
auth?.canDeleteServices) && (
|
||||
<>
|
||||
<DialogAction
|
||||
title="Delete Services"
|
||||
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
||||
type="destructive"
|
||||
onClick={handleBulkDelete}
|
||||
<Permissions permissions={["project:delete"]}>
|
||||
<DialogAction
|
||||
title="Delete Services"
|
||||
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
|
||||
type="destructive"
|
||||
onClick={handleBulkDelete}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start text-destructive"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DuplicateProject
|
||||
projectId={projectId}
|
||||
services={applications}
|
||||
selectedServiceIds={selectedServices}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<DuplicateProject
|
||||
projectId={projectId}
|
||||
services={applications}
|
||||
selectedServiceIds={selectedServices}
|
||||
/>
|
||||
</Permissions>
|
||||
|
||||
<Dialog
|
||||
open={isMoveDialogOpen}
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
@@ -54,6 +53,7 @@ import { useRouter } from "next/router";
|
||||
import { type ReactElement, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
|
||||
type TabState =
|
||||
| "projects"
|
||||
@@ -88,7 +88,6 @@ const Service = (
|
||||
);
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
@@ -179,9 +178,9 @@ const Service = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateApplication applicationId={applicationId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
<DeleteService id={applicationId} type="application" />
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
@@ -51,6 +50,7 @@ import { useRouter } from "next/router";
|
||||
import { type ReactElement, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
|
||||
type TabState =
|
||||
| "projects"
|
||||
@@ -78,7 +78,6 @@ const Service = (
|
||||
|
||||
const { data } = api.compose.one.useQuery({ composeId });
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
@@ -171,9 +170,9 @@ const Service = (
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateCompose composeId={composeId} />
|
||||
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
<DeleteService id={composeId} type="compose" />
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -44,6 +44,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
|
||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||
|
||||
@@ -57,7 +58,6 @@ const Mariadb = (
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -142,9 +142,9 @@ const Mariadb = (
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMariadb mariadbId={mariadbId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
<DeleteService id={mariadbId} type="mariadb" />
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -44,6 +44,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
|
||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||
|
||||
@@ -57,8 +58,6 @@ const Mongo = (
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mongo.one.useQuery({ mongoId });
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
@@ -143,9 +142,9 @@ const Mongo = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMongo mongoId={mongoId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
<DeleteService id={mongoId} type="mongo" />
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
@@ -44,6 +43,8 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||
|
||||
@@ -56,7 +57,6 @@ const MySql = (
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.mysql.one.useQuery({ mysqlId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -143,9 +143,9 @@ const MySql = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateMysql mysqlId={mysqlId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
<DeleteService id={mysqlId} type="mysql" />
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
@@ -44,6 +43,8 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||
|
||||
@@ -56,7 +57,6 @@ const Postgresql = (
|
||||
const { projectId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -142,9 +142,9 @@ const Postgresql = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdatePostgres postgresId={postgresId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
<DeleteService id={postgresId} type="postgres" />
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -43,6 +43,7 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
|
||||
type TabState = "projects" | "monitoring" | "settings" | "advanced";
|
||||
|
||||
@@ -56,8 +57,6 @@ const Redis = (
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.redis.one.useQuery({ redisId });
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
@@ -142,9 +141,9 @@ const Redis = (
|
||||
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateRedis redisId={redisId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<Permissions permissions={["service:delete"]}>
|
||||
<DeleteService id={redisId} type="redis" />
|
||||
)}
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
import { ShowApiKeys } from "@/components/dashboard/settings/api/show-api-keys";
|
||||
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import { Permissions } from "@/components/dashboard/shared/Permissions";
|
||||
|
||||
const Page = () => {
|
||||
const { data } = api.user.get.useQuery();
|
||||
|
||||
// const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<ProfileForm />
|
||||
{(data?.canAccessToAPI || data?.role === "owner") && <ShowApiKeys />}
|
||||
|
||||
{/* {isCloud && <RemoveSelfAccount />} */}
|
||||
<Permissions permissions={["api:access"]}>
|
||||
<ShowApiKeys />
|
||||
</Permissions>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -162,7 +162,7 @@ export const aiRouter = createTRPCRouter({
|
||||
deploy: protectedProcedure
|
||||
.input(deploySuggestionSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.session.activeOrganizationId,
|
||||
input.projectId,
|
||||
@@ -215,7 +215,7 @@ export const aiRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await addNewService(
|
||||
ctx.session.activeOrganizationId,
|
||||
ctx.user.ownerId,
|
||||
|
||||
@@ -63,7 +63,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
.input(apiCreateApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -88,7 +88,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
const newApplication = await createApplication(input);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newApplication.applicationId,
|
||||
@@ -110,7 +110,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.applicationId,
|
||||
@@ -201,7 +201,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
delete: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.applicationId,
|
||||
|
||||
@@ -64,7 +64,7 @@ export const composeRouter = createTRPCRouter({
|
||||
.input(apiCreateCompose)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -88,7 +88,7 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
const newService = await createCompose(input);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newService.composeId,
|
||||
@@ -105,7 +105,7 @@ export const composeRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.composeId,
|
||||
@@ -177,7 +177,7 @@ export const composeRouter = createTRPCRouter({
|
||||
delete: protectedProcedure
|
||||
.input(apiDeleteCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.composeId,
|
||||
@@ -469,7 +469,7 @@ export const composeRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -524,7 +524,7 @@ export const composeRouter = createTRPCRouter({
|
||||
isolatedDeployment: true,
|
||||
});
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
compose.composeId,
|
||||
|
||||
@@ -41,7 +41,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
.input(apiCreateMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -65,7 +65,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const newMariadb = await createMariadb(input);
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newMariadb.mariadbId,
|
||||
@@ -92,7 +92,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mariadbId,
|
||||
@@ -219,7 +219,7 @@ export const mariadbRouter = createTRPCRouter({
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneMariaDB)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mariadbId,
|
||||
|
||||
@@ -41,7 +41,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
.input(apiCreateMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -65,7 +65,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const newMongo = await createMongo(input);
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newMongo.mongoId,
|
||||
@@ -96,7 +96,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMongo)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mongoId,
|
||||
@@ -261,7 +261,7 @@ export const mongoRouter = createTRPCRouter({
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneMongo)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mongoId,
|
||||
|
||||
@@ -44,7 +44,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
.input(apiCreateMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -69,7 +69,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
const newMysql = await createMysql(input);
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newMysql.mysqlId,
|
||||
@@ -100,7 +100,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneMySql)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mysqlId,
|
||||
@@ -260,7 +260,7 @@ export const mysqlRouter = createTRPCRouter({
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneMySql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.mysqlId,
|
||||
|
||||
@@ -41,7 +41,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
.input(apiCreatePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -65,7 +65,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const newPostgres = await createPostgres(input);
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newPostgres.postgresId,
|
||||
@@ -96,7 +96,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOnePostgres)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.postgresId,
|
||||
@@ -244,7 +244,7 @@ export const postgresRouter = createTRPCRouter({
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOnePostgres)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.postgresId,
|
||||
|
||||
@@ -57,7 +57,7 @@ export const projectRouter = createTRPCRouter({
|
||||
.input(apiCreateProject)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"create",
|
||||
@@ -78,7 +78,7 @@ export const projectRouter = createTRPCRouter({
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await addNewProject(
|
||||
ctx.user.id,
|
||||
project.projectId,
|
||||
@@ -99,7 +99,7 @@ export const projectRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneProject)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
@@ -164,7 +164,7 @@ export const projectRouter = createTRPCRouter({
|
||||
return project;
|
||||
}),
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
const { accessedProjects, accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
@@ -241,7 +241,7 @@ export const projectRouter = createTRPCRouter({
|
||||
.input(apiRemoveProject)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"delete",
|
||||
@@ -314,7 +314,7 @@ export const projectRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkProjectAccess(
|
||||
ctx.user.id,
|
||||
"create",
|
||||
@@ -649,7 +649,10 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
if (!input.duplicateInSameProject && ctx.user.role === "member") {
|
||||
if (
|
||||
!input.duplicateInSameProject &&
|
||||
(ctx.user.role.name === "member" || !ctx.user.role.isSystem)
|
||||
) {
|
||||
await addNewProject(
|
||||
ctx.user.id,
|
||||
targetProject.projectId,
|
||||
|
||||
@@ -41,7 +41,7 @@ export const redisRouter = createTRPCRouter({
|
||||
.input(apiCreateRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.projectId,
|
||||
@@ -65,7 +65,7 @@ export const redisRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
const newRedis = await createRedis(input);
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await addNewService(
|
||||
ctx.user.id,
|
||||
newRedis.redisId,
|
||||
@@ -89,7 +89,7 @@ export const redisRouter = createTRPCRouter({
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneRedis)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.redisId,
|
||||
@@ -251,7 +251,7 @@ export const redisRouter = createTRPCRouter({
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneRedis)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user.role === "member") {
|
||||
if (ctx.user.role.name === "member" || !ctx.user.role.isSystem) {
|
||||
await checkServiceAccess(
|
||||
ctx.user.id,
|
||||
input.redisId,
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
createRoleSchema,
|
||||
role,
|
||||
updateRoleSchema,
|
||||
defaultPermissions,
|
||||
} from "@/server/db/schema";
|
||||
import { createRole, removeRoleById, updateRoleById } from "@dokploy/server";
|
||||
import { defaultPermissions } from "@dokploy/server/lib/permissions";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, asc, eq } from "drizzle-orm";
|
||||
|
||||
|
||||
@@ -30,7 +30,17 @@ import { ZodError } from "zod";
|
||||
*/
|
||||
|
||||
interface CreateContextOptions {
|
||||
user: (User & { role: "member" | "admin" | "owner"; ownerId: string }) | null;
|
||||
user:
|
||||
| (User & {
|
||||
role: {
|
||||
roleId: string;
|
||||
name: string;
|
||||
permissions: string[];
|
||||
isSystem: boolean;
|
||||
};
|
||||
ownerId: string;
|
||||
})
|
||||
| null;
|
||||
session:
|
||||
| (Session & { activeOrganizationId: string; impersonatedBy?: string })
|
||||
| null;
|
||||
@@ -182,7 +192,7 @@ export const uploadProcedure = async (opts: any) => {
|
||||
};
|
||||
|
||||
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
if (!ctx.session || !ctx.user || ctx.user.role !== "owner") {
|
||||
if (!ctx.session || !ctx.user || ctx.user.role.name !== "owner") {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
return next({
|
||||
@@ -196,7 +206,11 @@ export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
});
|
||||
|
||||
export const adminProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
if (!ctx.session || !ctx.user || ctx.user.role !== "owner") {
|
||||
if (
|
||||
!ctx.session ||
|
||||
!ctx.user ||
|
||||
(ctx.user.role.name !== "owner" && ctx.user.role.name !== "admin")
|
||||
) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
return next({
|
||||
|
||||
@@ -5,111 +5,6 @@ import { organization, member } from "./account";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
export const PERMISSIONS = {
|
||||
PROJECT: {
|
||||
VIEW: {
|
||||
name: "project:view",
|
||||
description: "View projects",
|
||||
},
|
||||
CREATE: {
|
||||
name: "project:create",
|
||||
description: "Create projects",
|
||||
},
|
||||
DELETE: {
|
||||
name: "project:delete",
|
||||
description: "Delete projects",
|
||||
},
|
||||
},
|
||||
SERVICE: {
|
||||
VIEW: {
|
||||
name: "service:view",
|
||||
description: "View services",
|
||||
},
|
||||
CREATE: {
|
||||
name: "service:create",
|
||||
description: "Create services",
|
||||
},
|
||||
DELETE: {
|
||||
name: "service:delete",
|
||||
description: "Delete services",
|
||||
},
|
||||
},
|
||||
TRAEFIK: {
|
||||
ACCESS: {
|
||||
name: "traefik_files:access",
|
||||
description: "Access traefik files",
|
||||
},
|
||||
},
|
||||
DOCKER: {
|
||||
VIEW: {
|
||||
name: "docker:view",
|
||||
description: "View docker",
|
||||
},
|
||||
},
|
||||
API: {
|
||||
ACCESS: {
|
||||
name: "api:access",
|
||||
description: "Access API",
|
||||
},
|
||||
},
|
||||
SCHEDULES: {
|
||||
ACCESS: {
|
||||
name: "schedules:access",
|
||||
description: "Access schedules",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const ownerPermissions = [
|
||||
PERMISSIONS.PROJECT.VIEW,
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.PROJECT.DELETE,
|
||||
PERMISSIONS.SERVICE.VIEW,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.SERVICE.DELETE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
PERMISSIONS.SCHEDULES.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const adminPermissions = [
|
||||
PERMISSIONS.PROJECT.VIEW,
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.PROJECT.DELETE,
|
||||
PERMISSIONS.SERVICE.VIEW,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.SERVICE.DELETE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
PERMISSIONS.DOCKER.VIEW,
|
||||
PERMISSIONS.API.ACCESS,
|
||||
PERMISSIONS.SCHEDULES.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const memberPermissions = [
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const defaultPermissions = [
|
||||
{
|
||||
name: "owner",
|
||||
description: "Owner of the organization with full access to all features",
|
||||
permissions: ownerPermissions,
|
||||
},
|
||||
{
|
||||
name: "admin",
|
||||
description:
|
||||
"Administrator with access to manage projects, services and configurations",
|
||||
permissions: adminPermissions,
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
description:
|
||||
"Regular member with access to create projects and manage services",
|
||||
permissions: memberPermissions,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const role = pgTable(
|
||||
"member_role",
|
||||
{
|
||||
|
||||
@@ -140,8 +140,18 @@ const { handler, api } = betterAuth({
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const ownerRole = await db.query.role.findFirst({
|
||||
where: and(eq(schema.role.name, "owner")),
|
||||
});
|
||||
|
||||
if (!ownerRole) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Owner role not found",
|
||||
});
|
||||
}
|
||||
|
||||
const isAdminPresent = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
where: and(eq(schema.member.roleId, ownerRole.roleId)),
|
||||
});
|
||||
if (isAdminPresent) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
@@ -152,8 +162,11 @@ const { handler, api } = betterAuth({
|
||||
}
|
||||
},
|
||||
after: async (user) => {
|
||||
const ownerRole = await db.query.role.findFirst({
|
||||
where: and(eq(schema.role.name, "owner")),
|
||||
});
|
||||
const isAdminPresent = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
where: and(eq(schema.member.roleId, ownerRole?.roleId || "")),
|
||||
});
|
||||
|
||||
if (!IS_CLOUD) {
|
||||
@@ -186,7 +199,6 @@ const { handler, api } = betterAuth({
|
||||
await tx.insert(schema.member).values({
|
||||
userId: user.id,
|
||||
organizationId: organization?.id || "",
|
||||
role: "owner",
|
||||
createdAt: new Date(),
|
||||
roleId: ownerRole?.roleId || "",
|
||||
});
|
||||
@@ -202,14 +214,18 @@ const { handler, api } = betterAuth({
|
||||
where: eq(schema.member.userId, session.userId),
|
||||
orderBy: desc(schema.member.createdAt),
|
||||
with: {
|
||||
role: true,
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(member);
|
||||
|
||||
return {
|
||||
data: {
|
||||
...session,
|
||||
activeOrganizationId: member?.organization.id,
|
||||
roleId: member?.roleId,
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -223,7 +239,7 @@ const { handler, api } = betterAuth({
|
||||
user: {
|
||||
modelName: "users",
|
||||
additionalFields: {
|
||||
role: {
|
||||
roleId: {
|
||||
type: "string",
|
||||
// required: true,
|
||||
input: false,
|
||||
@@ -331,6 +347,7 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
),
|
||||
with: {
|
||||
organization: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -363,7 +380,7 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
twoFactorEnabled,
|
||||
role: member?.role || "member",
|
||||
role: member?.role,
|
||||
ownerId: member?.organization.ownerId || apiKeyRecord.user.id,
|
||||
},
|
||||
};
|
||||
@@ -392,6 +409,15 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
};
|
||||
}
|
||||
|
||||
const mockSession = {
|
||||
session: {
|
||||
...session.session,
|
||||
},
|
||||
user: {
|
||||
...session.user,
|
||||
ownerId: session.user.ownerId,
|
||||
},
|
||||
};
|
||||
if (session?.user) {
|
||||
const member = await db.query.member.findFirst({
|
||||
where: and(
|
||||
@@ -402,17 +428,21 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
),
|
||||
),
|
||||
with: {
|
||||
role: true,
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
session.user.role = member?.role || "member";
|
||||
if (member?.role) {
|
||||
mockSession.user.role = member.role;
|
||||
}
|
||||
|
||||
if (member) {
|
||||
session.user.ownerId = member.organization.ownerId;
|
||||
mockSession.user.ownerId = member.organization.ownerId;
|
||||
} else {
|
||||
session.user.ownerId = session.user.id;
|
||||
mockSession.user.ownerId = session.user.id;
|
||||
}
|
||||
}
|
||||
|
||||
return session;
|
||||
return mockSession;
|
||||
};
|
||||
|
||||
131
packages/server/src/lib/permissions.ts
Normal file
131
packages/server/src/lib/permissions.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
export const PERMISSIONS = {
|
||||
PROJECT: {
|
||||
VIEW: {
|
||||
name: "project:view",
|
||||
description: "View projects",
|
||||
},
|
||||
CREATE: {
|
||||
name: "project:create",
|
||||
description: "Create projects",
|
||||
},
|
||||
DELETE: {
|
||||
name: "project:delete",
|
||||
description: "Delete projects",
|
||||
},
|
||||
},
|
||||
SERVICE: {
|
||||
VIEW: {
|
||||
name: "service:view",
|
||||
description: "View services",
|
||||
},
|
||||
CREATE: {
|
||||
name: "service:create",
|
||||
description: "Create services",
|
||||
},
|
||||
DELETE: {
|
||||
name: "service:delete",
|
||||
description: "Delete services",
|
||||
},
|
||||
},
|
||||
TRAEFIK: {
|
||||
ACCESS: {
|
||||
name: "traefik_files:access",
|
||||
description: "Access traefik files",
|
||||
},
|
||||
},
|
||||
DOCKER: {
|
||||
VIEW: {
|
||||
name: "docker:view",
|
||||
description: "View docker",
|
||||
},
|
||||
},
|
||||
API: {
|
||||
ACCESS: {
|
||||
name: "api:access",
|
||||
description: "Access API",
|
||||
},
|
||||
},
|
||||
SCHEDULES: {
|
||||
ACCESS: {
|
||||
name: "schedules:access",
|
||||
description: "Access schedules",
|
||||
},
|
||||
},
|
||||
GIT_PROVIDERS: {
|
||||
ACCESS: {
|
||||
name: "git_providers:access",
|
||||
description: "Access git providers",
|
||||
},
|
||||
},
|
||||
SSH_KEYS: {
|
||||
ACCESS: {
|
||||
name: "ssh_keys:access",
|
||||
description: "Access ssh keys",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const ownerPermissions = [
|
||||
PERMISSIONS.PROJECT.VIEW,
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.PROJECT.DELETE,
|
||||
PERMISSIONS.SERVICE.VIEW,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.SERVICE.DELETE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
PERMISSIONS.SCHEDULES.ACCESS,
|
||||
PERMISSIONS.GIT_PROVIDERS.ACCESS,
|
||||
PERMISSIONS.SSH_KEYS.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const adminPermissions = [
|
||||
PERMISSIONS.PROJECT.VIEW,
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.PROJECT.DELETE,
|
||||
PERMISSIONS.SERVICE.VIEW,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.SERVICE.DELETE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
PERMISSIONS.DOCKER.VIEW,
|
||||
PERMISSIONS.API.ACCESS,
|
||||
PERMISSIONS.SCHEDULES.ACCESS,
|
||||
PERMISSIONS.GIT_PROVIDERS.ACCESS,
|
||||
PERMISSIONS.SSH_KEYS.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const memberPermissions = [
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const defaultPermissions = [
|
||||
{
|
||||
name: "owner",
|
||||
description: "Owner of the organization with full access to all features",
|
||||
permissions: ownerPermissions,
|
||||
},
|
||||
{
|
||||
name: "admin",
|
||||
description:
|
||||
"Administrator with access to manage projects, services and configurations",
|
||||
permissions: adminPermissions,
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
description:
|
||||
"Regular member with access to create projects and manage services",
|
||||
permissions: memberPermissions,
|
||||
},
|
||||
] as const;
|
||||
|
||||
// Utility type to extract all permission names
|
||||
type ExtractPermissionNames<T> = T extends { name: infer U }
|
||||
? U
|
||||
: T extends object
|
||||
? {
|
||||
[K in keyof T]: ExtractPermissionNames<T[K]>;
|
||||
}[keyof T]
|
||||
: never;
|
||||
|
||||
export type PermissionName = ExtractPermissionNames<typeof PERMISSIONS>;
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
invitation,
|
||||
member,
|
||||
organization,
|
||||
role,
|
||||
users,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -13,9 +14,6 @@ import { findWebServer } from "./web-server";
|
||||
export const findUserById = async (userId: string) => {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.id, userId),
|
||||
// with: {
|
||||
// account: true,
|
||||
// },
|
||||
});
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
@@ -37,8 +35,12 @@ export const findOrganizationById = async (organizationId: string) => {
|
||||
};
|
||||
|
||||
export const isAdminPresent = async () => {
|
||||
const ownerRole = await db.query.role.findFirst({
|
||||
where: eq(role.name, "owner"),
|
||||
});
|
||||
|
||||
const admin = await db.query.member.findFirst({
|
||||
where: eq(member.role, "owner"),
|
||||
where: eq(member.roleId, ownerRole?.roleId || ""),
|
||||
});
|
||||
|
||||
if (!admin) {
|
||||
@@ -48,8 +50,12 @@ export const isAdminPresent = async () => {
|
||||
};
|
||||
|
||||
export const findOwner = async () => {
|
||||
const ownerRole = await db.query.role.findFirst({
|
||||
where: eq(role.name, "owner"),
|
||||
});
|
||||
|
||||
const owner = await db.query.member.findFirst({
|
||||
where: eq(member.role, "owner"),
|
||||
where: eq(member.roleId, ownerRole?.roleId || ""),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db";
|
||||
import {
|
||||
adminPermissions,
|
||||
type createRoleSchema,
|
||||
member,
|
||||
memberPermissions,
|
||||
ownerPermissions,
|
||||
role,
|
||||
type updateRoleSchema,
|
||||
} from "../db/schema";
|
||||
import type { z } from "zod";
|
||||
import {
|
||||
adminPermissions,
|
||||
memberPermissions,
|
||||
ownerPermissions,
|
||||
} from "../lib/permissions";
|
||||
|
||||
export const createRole = async (
|
||||
input: z.infer<typeof createRoleSchema>,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { apikey, member, users } from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { auth } from "../lib/auth";
|
||||
import { PERMISSIONS } from "../lib/permissions";
|
||||
|
||||
export type User = typeof users.$inferSelect;
|
||||
|
||||
@@ -44,13 +45,16 @@ export const canPerformCreationService = async (
|
||||
projectId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { accessedProjects, canCreateServices } = await findMemberById(
|
||||
const { accessedProjects, role } = await findMemberById(
|
||||
userId,
|
||||
organizationId,
|
||||
);
|
||||
const haveAccessToProject = accessedProjects.includes(projectId);
|
||||
|
||||
if (canCreateServices && haveAccessToProject) {
|
||||
if (
|
||||
role?.permissions?.includes(PERMISSIONS.SERVICE.CREATE.name) &&
|
||||
haveAccessToProject
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -77,13 +81,16 @@ export const canPeformDeleteService = async (
|
||||
serviceId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { accessedServices, canDeleteServices } = await findMemberById(
|
||||
const { accessedServices, role } = await findMemberById(
|
||||
userId,
|
||||
organizationId,
|
||||
);
|
||||
const haveAccessToService = accessedServices.includes(serviceId);
|
||||
|
||||
if (canDeleteServices && haveAccessToService) {
|
||||
if (
|
||||
role?.permissions?.includes(PERMISSIONS.SERVICE.DELETE.name) &&
|
||||
haveAccessToService
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -94,9 +101,9 @@ export const canPerformCreationProject = async (
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { canCreateProjects } = await findMemberById(userId, organizationId);
|
||||
const { role } = await findMemberById(userId, organizationId);
|
||||
|
||||
if (canCreateProjects) {
|
||||
if (role?.permissions?.includes(PERMISSIONS.PROJECT.CREATE.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -107,9 +114,9 @@ export const canPerformDeleteProject = async (
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { canDeleteProjects } = await findMemberById(userId, organizationId);
|
||||
const { role } = await findMemberById(userId, organizationId);
|
||||
|
||||
if (canDeleteProjects) {
|
||||
if (role?.permissions?.includes(PERMISSIONS.PROJECT.DELETE.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -135,11 +142,8 @@ export const canAccessToTraefikFiles = async (
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { canAccessToTraefikFiles } = await findMemberById(
|
||||
userId,
|
||||
organizationId,
|
||||
);
|
||||
return canAccessToTraefikFiles;
|
||||
const { role } = await findMemberById(userId, organizationId);
|
||||
return role?.permissions?.includes(PERMISSIONS.TRAEFIK.ACCESS.name);
|
||||
};
|
||||
|
||||
export const checkServiceAccess = async (
|
||||
@@ -183,7 +187,7 @@ export const checkServiceAccess = async (
|
||||
};
|
||||
|
||||
export const checkProjectAccess = async (
|
||||
authId: string,
|
||||
userId: string,
|
||||
action: "create" | "delete" | "access",
|
||||
organizationId: string,
|
||||
projectId?: string,
|
||||
@@ -192,16 +196,16 @@ export const checkProjectAccess = async (
|
||||
switch (action) {
|
||||
case "access":
|
||||
hasPermission = await canPerformAccessProject(
|
||||
authId,
|
||||
userId,
|
||||
projectId as string,
|
||||
organizationId,
|
||||
);
|
||||
break;
|
||||
case "create":
|
||||
hasPermission = await canPerformCreationProject(authId, organizationId);
|
||||
hasPermission = await canPerformCreationProject(userId, organizationId);
|
||||
break;
|
||||
case "delete":
|
||||
hasPermission = await canPerformDeleteProject(authId, organizationId);
|
||||
hasPermission = await canPerformDeleteProject(userId, organizationId);
|
||||
break;
|
||||
default:
|
||||
hasPermission = false;
|
||||
@@ -225,6 +229,7 @@ export const findMemberById = async (
|
||||
),
|
||||
with: {
|
||||
user: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user