diff --git a/apps/dokploy/components/dashboard/settings/users/add-invitation.tsx b/apps/dokploy/components/dashboard/settings/users/add-invitation.tsx index 6e0384554..e778f2e96 100644 --- a/apps/dokploy/components/dashboard/settings/users/add-invitation.tsx +++ b/apps/dokploy/components/dashboard/settings/users/add-invitation.tsx @@ -158,6 +158,7 @@ export const AddInvitation = () => { Member + Admin diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index d1d4ae273..b4806c4fc 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -156,7 +156,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 === "owner" || auth?.role === "admin"), }, { isSingle: true, @@ -166,7 +167,9 @@ 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 === "owner" || + auth?.role === "admin" || + auth?.canAccessToTraefikFiles) && !isCloud ), }, @@ -177,7 +180,12 @@ 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 === "owner" || + auth?.role === "admin" || + auth?.canAccessToDocker) && + !isCloud + ), }, { isSingle: true, @@ -186,7 +194,12 @@ 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 === "owner" || + auth?.role === "admin" || + auth?.canAccessToDocker) && + !isCloud + ), }, { isSingle: true, @@ -195,7 +208,12 @@ 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 === "owner" || + auth?.role === "admin" || + auth?.canAccessToDocker) && + !isCloud + ), }, // Legacy unused menu, adjusted to the new structure @@ -262,7 +280,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 === "owner" || auth?.role === "admin") && !isCloud), }, { isSingle: true, @@ -276,7 +295,8 @@ const MENU: Menu = { url: "/dashboard/settings/servers", icon: Server, // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), + isEnabled: ({ auth }) => + !!(auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, @@ -284,7 +304,8 @@ const MENU: Menu = { icon: Users, url: "/dashboard/settings/users", // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), + isEnabled: ({ auth }) => + !!(auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, @@ -300,7 +321,8 @@ const MENU: Menu = { icon: BotIcon, url: "/dashboard/settings/ai", isSingle: true, - isEnabled: ({ auth }) => !!(auth?.role === "owner"), + isEnabled: ({ auth }) => + !!(auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, @@ -317,7 +339,8 @@ const MENU: Menu = { url: "/dashboard/settings/registry", icon: Package, // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), + isEnabled: ({ auth }) => + !!(auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, @@ -325,7 +348,8 @@ const MENU: Menu = { url: "/dashboard/settings/destinations", icon: Database, // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), + isEnabled: ({ auth }) => + !!(auth?.role === "owner" || auth?.role === "admin"), }, { @@ -334,7 +358,8 @@ const MENU: Menu = { url: "/dashboard/settings/certificates", icon: ShieldCheck, // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), + isEnabled: ({ auth }) => + !!(auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, @@ -342,7 +367,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 === "owner" || auth?.role === "admin") && !isCloud), }, { isSingle: true, @@ -350,7 +376,8 @@ const MENU: Menu = { url: "/dashboard/settings/notifications", icon: Bell, // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), + isEnabled: ({ auth }) => + !!(auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, @@ -358,7 +385,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 === "owner" || auth?.role === "admin") && isCloud), }, ], @@ -654,7 +682,9 @@ function SidebarLogo() { )} ))} - {(user?.role === "owner" || isCloud) && ( + {(user?.role === "owner" || + user?.role === "admin" || + isCloud) && ( <> @@ -1018,7 +1048,7 @@ export default function Page({ children }: Props) { - {!isCloud && auth?.role === "owner" && ( + {!isCloud && (auth?.role === "owner" || auth?.role === "admin") && ( diff --git a/apps/dokploy/components/layouts/user-nav.tsx b/apps/dokploy/components/layouts/user-nav.tsx index e476a5f50..07605f08c 100644 --- a/apps/dokploy/components/layouts/user-nav.tsx +++ b/apps/dokploy/components/layouts/user-nav.tsx @@ -101,7 +101,9 @@ export const UserNav = () => { > Monitoring - {(data?.role === "owner" || data?.canAccessToTraefikFiles) && ( + {(data?.role === "owner" || + data?.role === "admin" || + data?.canAccessToTraefikFiles) && ( { @@ -111,7 +113,9 @@ export const UserNav = () => { Traefik )} - {(data?.role === "owner" || data?.canAccessToDocker) && ( + {(data?.role === "owner" || + data?.role === "admin" || + data?.canAccessToDocker) && ( { @@ -125,7 +129,7 @@ export const UserNav = () => { )} ) : ( - data?.role === "owner" && ( + (data?.role === "owner" || data?.role === "admin") && ( { diff --git a/apps/dokploy/pages/dashboard/schedules.tsx b/apps/dokploy/pages/dashboard/schedules.tsx index 17d04b29a..113b079e8 100644 --- a/apps/dokploy/pages/dashboard/schedules.tsx +++ b/apps/dokploy/pages/dashboard/schedules.tsx @@ -40,7 +40,7 @@ export async function getServerSideProps( }; } const { user } = await validateRequest(ctx.req); - if (!user || user.role !== "owner") { + if (!user || (user.role !== "owner" && user.role !== "admin")) { return { redirect: { permanent: true, diff --git a/apps/dokploy/pages/dashboard/settings/profile.tsx b/apps/dokploy/pages/dashboard/settings/profile.tsx index 90cc345e4..34f8126e4 100644 --- a/apps/dokploy/pages/dashboard/settings/profile.tsx +++ b/apps/dokploy/pages/dashboard/settings/profile.tsx @@ -18,7 +18,9 @@ const Page = () => {
- {(data?.canAccessToAPI || data?.role === "owner") && } + {(data?.canAccessToAPI || + data?.role === "owner" || + data?.role === "admin") && } {/* {isCloud && } */}
diff --git a/apps/dokploy/server/api/routers/organization.ts b/apps/dokploy/server/api/routers/organization.ts index a015310d1..0f3d1c82e 100644 --- a/apps/dokploy/server/api/routers/organization.ts +++ b/apps/dokploy/server/api/routers/organization.ts @@ -15,7 +15,7 @@ export const organizationRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { - if (ctx.user.role !== "owner" && !IS_CLOUD) { + if (ctx.user.role !== "owner" && ctx.user.role !== "admin" && !IS_CLOUD) { throw new TRPCError({ code: "FORBIDDEN", message: "Only the organization owner can create an organization", @@ -86,7 +86,7 @@ export const organizationRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { - if (ctx.user.role !== "owner" && !IS_CLOUD) { + if (ctx.user.role !== "owner" && ctx.user.role !== "admin" && !IS_CLOUD) { throw new TRPCError({ code: "FORBIDDEN", message: "Only the organization owner can update it", @@ -109,7 +109,7 @@ export const organizationRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { - if (ctx.user.role !== "owner" && !IS_CLOUD) { + if (ctx.user.role !== "owner" && ctx.user.role !== "admin" && !IS_CLOUD) { throw new TRPCError({ code: "FORBIDDEN", message: "Only the organization owner can delete it", diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 02678b990..dc084a735 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -201,7 +201,7 @@ export const settingsRouter = createTRPCRouter({ if (IS_CLOUD) { return true; } - await updateUser(ctx.user.id, { + await updateUser(ctx.user.ownerId, { sshPrivateKey: input.sshPrivateKey, }); @@ -213,7 +213,7 @@ export const settingsRouter = createTRPCRouter({ if (IS_CLOUD) { return true; } - const user = await updateUser(ctx.user.id, { + const user = await updateUser(ctx.user.ownerId, { host: input.host, ...(input.letsEncryptEmail && { letsEncryptEmail: input.letsEncryptEmail, @@ -240,7 +240,7 @@ export const settingsRouter = createTRPCRouter({ if (IS_CLOUD) { return true; } - await updateUser(ctx.user.id, { + await updateUser(ctx.user.ownerId, { sshPrivateKey: null, }); return true; @@ -300,7 +300,7 @@ export const settingsRouter = createTRPCRouter({ } } } else if (!IS_CLOUD) { - const userUpdated = await updateUser(ctx.user.id, { + const userUpdated = await updateUser(ctx.user.ownerId, { enableDockerCleanup: input.enableDockerCleanup, }); diff --git a/apps/dokploy/server/api/routers/stripe.ts b/apps/dokploy/server/api/routers/stripe.ts index 288924436..d2a000324 100644 --- a/apps/dokploy/server/api/routers/stripe.ts +++ b/apps/dokploy/server/api/routers/stripe.ts @@ -56,15 +56,16 @@ export const stripeRouter = createTRPCRouter({ }); const items = getStripeItems(input.serverQuantity, input.isAnnual); - const user = await findUserById(ctx.user.id); + // Always operate on the organization owner's Stripe customer + const owner = await findUserById(ctx.user.ownerId); - let stripeCustomerId = user.stripeCustomerId; + let stripeCustomerId = owner.stripeCustomerId; if (stripeCustomerId) { const customer = await stripe.customers.retrieve(stripeCustomerId); if (customer.deleted) { - await updateUser(user.id, { + await updateUser(owner.id, { stripeCustomerId: null, }); stripeCustomerId = null; @@ -78,7 +79,7 @@ export const stripeRouter = createTRPCRouter({ customer: stripeCustomerId, }), metadata: { - adminId: user.id, + adminId: owner.id, }, allow_promotion_codes: true, success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`, @@ -88,15 +89,16 @@ export const stripeRouter = createTRPCRouter({ return { sessionId: session.id }; }), createCustomerPortalSession: adminProcedure.mutation(async ({ ctx }) => { - const user = await findUserById(ctx.user.id); + // Use the organization's owner account for billing portal + const owner = await findUserById(ctx.user.ownerId); - if (!user.stripeCustomerId) { + if (!owner.stripeCustomerId) { throw new TRPCError({ code: "BAD_REQUEST", message: "Stripe Customer ID not found", }); } - const stripeCustomerId = user.stripeCustomerId; + const stripeCustomerId = owner.stripeCustomerId; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2024-09-30.acacia", diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index 2e7c7a0c5..fb113f566 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -86,7 +86,11 @@ export const userRouter = createTRPCRouter({ // Allow access if: // 1. User is requesting their own information // 2. User has owner role (admin permissions) AND user is in the same organization - if (memberResult.userId !== ctx.user.id && ctx.user.role !== "owner") { + if ( + memberResult.userId !== ctx.user.id && + ctx.user.role !== "owner" && + ctx.user.role !== "admin" + ) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized to access this user", diff --git a/apps/dokploy/server/api/trpc.ts b/apps/dokploy/server/api/trpc.ts index c99f9104d..7b4e2e3f0 100644 --- a/apps/dokploy/server/api/trpc.ts +++ b/apps/dokploy/server/api/trpc.ts @@ -183,7 +183,11 @@ 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 !== "owner" && ctx.user.role !== "admin") + ) { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({ @@ -197,7 +201,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 !== "owner" && ctx.user.role !== "admin") + ) { throw new TRPCError({ code: "UNAUTHORIZED" }); } return next({