- {(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({