diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 03ebe7a85..44e91d6e6 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -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 = () => { - {(auth?.role === "owner" || auth?.canCreateProjects) && ( +
- )} +
@@ -289,8 +290,11 @@ export const ShowProjects = () => {
e.stopPropagation()} > - {(auth?.role === "owner" || - auth?.canDeleteProjects) && ( + { - )} +
diff --git a/apps/dokploy/components/dashboard/settings/billing/show-welcome-dokploy.tsx b/apps/dokploy/components/dashboard/settings/billing/show-welcome-dokploy.tsx index 64362b25c..34620ea5d 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-welcome-dokploy.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-welcome-dokploy.tsx @@ -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 diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 59e4736de..37558a4a5 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -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({ 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" >
+ ( + + Name + + + + + + )} + /> ; - -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({ - 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 ( - - - e.preventDefault()} - > - Add Permissions - - - - - Permissions - Add or remove permissions - - {isError && {error?.message}} - -
- - ( - -
- Create Projects - - Allow the user to create projects - -
- - - -
- )} - /> - ( - -
- Delete Projects - - Allow the user to delete projects - -
- - - -
- )} - /> - ( - -
- Create Services - - Allow the user to create services - -
- - - -
- )} - /> - ( - -
- Delete Services - - Allow the user to delete services - -
- - - -
- )} - /> - ( - -
- Access to Traefik Files - - Allow the user to access to the Traefik Tab Files - -
- - - -
- )} - /> - ( - -
- Access to Docker - - Allow the user to access to the Docker Tab - -
- - - -
- )} - /> - ( - -
- Access to API/CLI - - Allow the user to access to the API/CLI - -
- - - -
- )} - /> - ( - -
- Access to SSH Keys - - Allow to users to access to the SSH Keys section - -
- - - -
- )} - /> - ( - -
- Access to Git Providers - - Allow to users to access to the Git Providers section - -
- - - -
- )} - /> - ( - -
- Projects - - Select the Projects that the user can access - -
- {projects?.length === 0 && ( -

- No projects found -

- )} -
- {projects?.map((item, index) => { - const applications = extractServices(item); - return ( - { - return ( - -
- - { - return checked - ? field.onChange([ - ...(field.value || []), - item.projectId, - ]) - : field.onChange( - field.value?.filter( - (value) => - value !== item.projectId, - ), - ); - }} - /> - - - {item.name} - -
- {applications.length === 0 && ( -

- No services found -

- )} - {applications?.map((item, index) => ( - { - return ( - - - { - return checked - ? field.onChange([ - ...(field.value || []), - item.id, - ]) - : field.onChange( - field.value?.filter( - (value) => - value !== item.id, - ), - ); - }} - /> - - - {item.name} - - - ); - }} - /> - ))} -
- ); - }} - /> - ); - })} -
- - -
- )} - /> - - - - - -
-
- ); -}; diff --git a/apps/dokploy/components/dashboard/settings/users/show-users.tsx b/apps/dokploy/components/dashboard/settings/users/show-users.tsx index 62bcdd23a..e1e355438 100644 --- a/apps/dokploy/components/dashboard/settings/users/show-users.tsx +++ b/apps/dokploy/components/dashboard/settings/users/show-users.tsx @@ -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 = () => { <> - diff --git a/apps/dokploy/components/dashboard/shared/Permissions.tsx b/apps/dokploy/components/dashboard/shared/Permissions.tsx new file mode 100644 index 000000000..18006847b --- /dev/null +++ b/apps/dokploy/components/dashboard/shared/Permissions.tsx @@ -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}; +}; diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index bca25803b..18d3430d5 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -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["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() { )}
))} - {(user?.role === "owner" || isCloud) && ( + {(user?.role?.name === "owner" || isCloud) && ( <> @@ -961,7 +989,7 @@ export default function Page({ children }: Props) { - {!isCloud && auth?.role === "owner" && ( + {!isCloud && auth?.role?.name === "owner" && ( diff --git a/apps/dokploy/components/layouts/user-nav.tsx b/apps/dokploy/components/layouts/user-nav.tsx index 05c601f6e..48680fdc9 100644 --- a/apps/dokploy/components/layouts/user-nav.tsx +++ b/apps/dokploy/components/layouts/user-nav.tsx @@ -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 - {(data?.role === "owner" || data?.canAccessToTraefikFiles) && ( + { @@ -107,8 +109,9 @@ export const UserNav = () => { > Traefik - )} - {(data?.role === "owner" || data?.canAccessToDocker) && ( + + + { @@ -119,11 +122,11 @@ export const UserNav = () => { > Docker - )} + ) : ( <> - {data?.role === "owner" && ( + {data?.role?.name === "owner" && ( { @@ -136,7 +139,7 @@ export const UserNav = () => { )} - {isCloud && data?.role === "owner" && ( + {isCloud && data?.role?.name === "owner" && ( { @@ -154,9 +157,6 @@ export const UserNav = () => { await authClient.signOut().then(() => { router.push("/"); }); - // await mutateAsync().then(() => { - // router.push("/"); - // }); }} > Log out diff --git a/apps/dokploy/pages/dashboard/docker.tsx b/apps/dokploy/pages/dashboard/docker.tsx index 56154d10d..23ef1db14 100644 --- a/apps/dokploy/pages/dashboard/docker.tsx +++ b/apps/dokploy/pages/dashboard/docker.tsx @@ -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, diff --git a/apps/dokploy/pages/dashboard/project/[projectId].tsx b/apps/dokploy/pages/dashboard/project/[projectId].tsx index e189ed5cc..1b899c986 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId].tsx @@ -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(() => { if (typeof window !== "undefined") { return localStorage.getItem("servicesSort") || "createdAt-desc"; @@ -736,30 +736,27 @@ const Project = ( Stop - {(auth?.role === "owner" || - auth?.canDeleteServices) && ( - <> - + + - - - - )} + + Delete + + + + @@ -179,9 +178,9 @@ const Service = (
- {(auth?.role === "owner" || auth?.canDeleteServices) && ( + - )} +
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index f7e793a64..1587a4d3c 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -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 = (
- {(auth?.role === "owner" || auth?.canDeleteServices) && ( + - )} +
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx index d6745a241..a72403b78 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx @@ -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(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 = (
- {(auth?.role === "owner" || auth?.canDeleteServices) && ( + - )} +
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx index 82b60d5ea..ef2cd43c6 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx @@ -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(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 = (
- {(auth?.role === "owner" || auth?.canDeleteServices) && ( + - )} +
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx index 11efc6528..cfc12659e 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx @@ -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(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 = (
- {(auth?.role === "owner" || auth?.canDeleteServices) && ( + - )} +
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx index 99e8e4787..5e8befcde 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx @@ -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(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 = (
- {(auth?.role === "owner" || auth?.canDeleteServices) && ( + - )} +
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx index cc9a4235c..0f7576e68 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx @@ -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(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 = (
- {(auth?.role === "owner" || auth?.canDeleteServices) && ( + - )} +
diff --git a/apps/dokploy/pages/dashboard/settings/profile.tsx b/apps/dokploy/pages/dashboard/settings/profile.tsx index 83ff5624c..7ef563acb 100644 --- a/apps/dokploy/pages/dashboard/settings/profile.tsx +++ b/apps/dokploy/pages/dashboard/settings/profile.tsx @@ -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 (
- {(data?.canAccessToAPI || data?.role === "owner") && } - - {/* {isCloud && } */} + + +
); diff --git a/apps/dokploy/server/api/routers/ai.ts b/apps/dokploy/server/api/routers/ai.ts index 91ba35622..c490b2522 100644 --- a/apps/dokploy/server/api/routers/ai.ts +++ b/apps/dokploy/server/api/routers/ai.ts @@ -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, diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index fed80ef80..65d0b7c28 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -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, diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index bd838afeb..90182e89c 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -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, diff --git a/apps/dokploy/server/api/routers/mariadb.ts b/apps/dokploy/server/api/routers/mariadb.ts index dd3b1bf78..699431864 100644 --- a/apps/dokploy/server/api/routers/mariadb.ts +++ b/apps/dokploy/server/api/routers/mariadb.ts @@ -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, diff --git a/apps/dokploy/server/api/routers/mongo.ts b/apps/dokploy/server/api/routers/mongo.ts index 4d815c0aa..e65012350 100644 --- a/apps/dokploy/server/api/routers/mongo.ts +++ b/apps/dokploy/server/api/routers/mongo.ts @@ -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, diff --git a/apps/dokploy/server/api/routers/mysql.ts b/apps/dokploy/server/api/routers/mysql.ts index ee1b80dc8..ca9377e7d 100644 --- a/apps/dokploy/server/api/routers/mysql.ts +++ b/apps/dokploy/server/api/routers/mysql.ts @@ -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, diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index 9eef11f8f..8a095e0dc 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -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, diff --git a/apps/dokploy/server/api/routers/project.ts b/apps/dokploy/server/api/routers/project.ts index 5744a103e..2444a3a10 100644 --- a/apps/dokploy/server/api/routers/project.ts +++ b/apps/dokploy/server/api/routers/project.ts @@ -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, diff --git a/apps/dokploy/server/api/routers/redis.ts b/apps/dokploy/server/api/routers/redis.ts index 0aafac8e1..f1b3ccf25 100644 --- a/apps/dokploy/server/api/routers/redis.ts +++ b/apps/dokploy/server/api/routers/redis.ts @@ -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, diff --git a/apps/dokploy/server/api/routers/role.ts b/apps/dokploy/server/api/routers/role.ts index 8b928e873..841c18346 100644 --- a/apps/dokploy/server/api/routers/role.ts +++ b/apps/dokploy/server/api/routers/role.ts @@ -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"; diff --git a/apps/dokploy/server/api/trpc.ts b/apps/dokploy/server/api/trpc.ts index afd620540..fa7e740b8 100644 --- a/apps/dokploy/server/api/trpc.ts +++ b/apps/dokploy/server/api/trpc.ts @@ -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({ diff --git a/packages/server/src/db/schema/rbac.ts b/packages/server/src/db/schema/rbac.ts index 937a4f7da..0ff2fbbc9 100644 --- a/packages/server/src/db/schema/rbac.ts +++ b/packages/server/src/db/schema/rbac.ts @@ -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", { diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index e8b199656..2dc758e95 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -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; }; diff --git a/packages/server/src/lib/permissions.ts b/packages/server/src/lib/permissions.ts new file mode 100644 index 000000000..49a3da974 --- /dev/null +++ b/packages/server/src/lib/permissions.ts @@ -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 extends { name: infer U } + ? U + : T extends object + ? { + [K in keyof T]: ExtractPermissionNames; + }[keyof T] + : never; + +export type PermissionName = ExtractPermissionNames; diff --git a/packages/server/src/services/admin.ts b/packages/server/src/services/admin.ts index a00aca631..59977e8f2 100644 --- a/packages/server/src/services/admin.ts +++ b/packages/server/src/services/admin.ts @@ -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, }, diff --git a/packages/server/src/services/role.ts b/packages/server/src/services/role.ts index 943c47f3d..c13f523e7 100644 --- a/packages/server/src/services/role.ts +++ b/packages/server/src/services/role.ts @@ -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, diff --git a/packages/server/src/services/user.ts b/packages/server/src/services/user.ts index d9f1e090d..473afa3da 100644 --- a/packages/server/src/services/user.ts +++ b/packages/server/src/services/user.ts @@ -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, }, });