From 6c3230648a586b18588d107b8f4bcdf5dea7152c Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 18 Feb 2026 01:34:07 -0600 Subject: [PATCH 1/5] refactor(sso): update trusted origins handling and introduce license validation - Replaced user data fetching with a dedicated query for trusted origins in SSO settings. - Updated mutation functions to utilize the new trusted origins query. - Introduced a new service function to validate enterprise licenses based on organization ownership. - Enhanced SSO router to ensure trusted origins are managed by the organization owner. - Added callback URL for email sign-in in the home page. --- .../proprietary/sso/sso-settings.tsx | 15 ++-- apps/dokploy/pages/index.tsx | 2 +- .../api/routers/proprietary/license-key.ts | 15 +--- .../server/api/routers/proprietary/sso.ts | 84 +++++++++++++++---- apps/dokploy/server/api/routers/user.ts | 8 ++ apps/dokploy/server/api/trpc.ts | 10 ++- packages/server/src/index.ts | 1 + .../src/services/proprietary/license-key.ts | 29 +++++++ .../server/src/services/proprietary/sso.ts | 11 +++ 9 files changed, 132 insertions(+), 43 deletions(-) create mode 100644 packages/server/src/services/proprietary/license-key.ts diff --git a/apps/dokploy/components/proprietary/sso/sso-settings.tsx b/apps/dokploy/components/proprietary/sso/sso-settings.tsx index bb8330cda..dee41b13c 100644 --- a/apps/dokploy/components/proprietary/sso/sso-settings.tsx +++ b/apps/dokploy/components/proprietary/sso/sso-settings.tsx @@ -84,9 +84,10 @@ export const SSOSettings = () => { const [newOriginInput, setNewOriginInput] = useState(""); const { data: providers, isLoading } = api.sso.listProviders.useQuery(); - const { data: userData } = api.user.get.useQuery(undefined, { - enabled: manageOriginsOpen, - }); + const { data: trustedOrigins = [] } = api.sso.getTrustedOrigins.useQuery( + undefined, + { enabled: manageOriginsOpen }, + ); const { mutateAsync: deleteProvider, isLoading: isDeleting } = api.sso.deleteProvider.useMutation(); const { mutateAsync: addTrustedOrigin, isLoading: isAddingOrigin } = @@ -96,8 +97,6 @@ export const SSOSettings = () => { const { mutateAsync: updateTrustedOrigin, isLoading: isUpdatingOrigin } = api.sso.updateTrustedOrigin.useMutation(); - const trustedOrigins = userData?.user?.trustedOrigins ?? []; - const handleAddOrigin = async () => { const value = newOriginInput.trim(); if (!value) return; @@ -105,7 +104,7 @@ export const SSOSettings = () => { await addTrustedOrigin({ origin: value }); toast.success("Trusted origin added"); setNewOriginInput(""); - await utils.user.get.invalidate(); + await utils.sso.getTrustedOrigins.invalidate(); } catch (err) { toast.error( err instanceof Error ? err.message : "Failed to add trusted origin", @@ -118,7 +117,7 @@ export const SSOSettings = () => { await removeTrustedOrigin({ origin }); toast.success("Trusted origin removed"); if (editingOrigin === origin) setEditingOrigin(null); - await utils.user.get.invalidate(); + await utils.sso.getTrustedOrigins.invalidate(); } catch (err) { toast.error( err instanceof Error ? err.message : "Failed to remove trusted origin", @@ -144,7 +143,7 @@ export const SSOSettings = () => { toast.success("Trusted origin updated"); setEditingOrigin(null); setEditingValue(""); - await utils.user.get.invalidate(); + await utils.sso.getTrustedOrigins.invalidate(); } catch (err) { toast.error( err instanceof Error ? err.message : "Failed to update trusted origin", diff --git a/apps/dokploy/pages/index.tsx b/apps/dokploy/pages/index.tsx index a88a5dc8e..a778f50f4 100644 --- a/apps/dokploy/pages/index.tsx +++ b/apps/dokploy/pages/index.tsx @@ -81,6 +81,7 @@ export default function Home({ IS_CLOUD }: Props) { const { data, error } = await authClient.signIn.email({ email: values.email, password: values.password, + callbackURL: "/dashboard/projects", }); if (error) { @@ -105,7 +106,6 @@ export default function Home({ IS_CLOUD }: Props) { setIsLoginLoading(false); } }; - const onTwoFactorSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (twoFactorCode.length !== 6) { diff --git a/apps/dokploy/server/api/routers/proprietary/license-key.ts b/apps/dokploy/server/api/routers/proprietary/license-key.ts index ec7ad55c8..d6a770be9 100644 --- a/apps/dokploy/server/api/routers/proprietary/license-key.ts +++ b/apps/dokploy/server/api/routers/proprietary/license-key.ts @@ -1,5 +1,5 @@ import { user } from "@dokploy/server/db/schema"; -import { validateLicenseKey } from "@dokploy/server/index"; +import { hasValidLicense, validateLicenseKey } from "@dokploy/server/index"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { z } from "zod"; @@ -184,18 +184,7 @@ export const licenseKeyRouter = createTRPCRouter({ }; }), haveValidLicenseKey: adminProcedure.query(async ({ ctx }) => { - const currentUserId = ctx.user.id; - const currentUser = await db.query.user.findFirst({ - where: eq(user.id, currentUserId), - columns: { - enableEnterpriseFeatures: true, - isValidEnterpriseLicense: true, - }, - }); - return !!( - currentUser?.enableEnterpriseFeatures && - currentUser?.isValidEnterpriseLicense - ); + return await hasValidLicense(ctx.session.activeOrganizationId); }), updateEnterpriseSettings: adminProcedure .input( diff --git a/apps/dokploy/server/api/routers/proprietary/sso.ts b/apps/dokploy/server/api/routers/proprietary/sso.ts index d59b2c974..c8b42f7fb 100644 --- a/apps/dokploy/server/api/routers/proprietary/sso.ts +++ b/apps/dokploy/server/api/routers/proprietary/sso.ts @@ -2,7 +2,10 @@ import { normalizeTrustedOrigin } from "@dokploy/server"; import { IS_CLOUD } from "@dokploy/server/constants"; import { member, ssoProvider, user } from "@dokploy/server/db/schema"; import { ssoProviderBodySchema } from "@dokploy/server/db/schema/sso"; -import { requestToHeaders } from "@dokploy/server/index"; +import { + getOrganizationOwnerId, + requestToHeaders, +} from "@dokploy/server/index"; import { auth } from "@dokploy/server/lib/auth"; import { TRPCError } from "@trpc/server"; import { and, asc, eq } from "drizzle-orm"; @@ -59,6 +62,17 @@ export const ssoRouter = createTRPCRouter({ }); return providers; }), + getTrustedOrigins: enterpriseProcedure.query(async ({ ctx }) => { + const ownerId = await getOrganizationOwnerId( + ctx.session.activeOrganizationId, + ); + if (!ownerId) return []; + const ownerUser = await db.query.user.findFirst({ + where: eq(user.id, ownerId), + columns: { trustedOrigins: true }, + }); + return ownerUser?.trustedOrigins ?? []; + }), one: enterpriseProcedure .input(z.object({ providerId: z.string().min(1) })) .query(async ({ ctx, input }) => { @@ -135,11 +149,20 @@ export const ssoRouter = createTRPCRouter({ normalizeTrustedOrigin(existing.issuer) !== normalizeTrustedOrigin(input.issuer); if (issuerChanged) { - const currentUser = await db.query.user.findFirst({ - where: eq(user.id, ctx.session.userId), + const ownerId = await getOrganizationOwnerId( + ctx.session.activeOrganizationId, + ); + if (!ownerId) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Organization owner not found", + }); + } + const ownerUser = await db.query.user.findFirst({ + where: eq(user.id, ownerId), columns: { trustedOrigins: true }, }); - const trustedOrigins = currentUser?.trustedOrigins ?? []; + const trustedOrigins = ownerUser?.trustedOrigins ?? []; const newOrigin = normalizeTrustedOrigin(input.issuer); const isInTrustedOrigins = trustedOrigins.some( (o) => o.toLowerCase() === newOrigin.toLowerCase(), @@ -148,7 +171,7 @@ export const ssoRouter = createTRPCRouter({ throw new TRPCError({ code: "BAD_REQUEST", message: - "The new Issuer URL is not in your trusted origins list. Please add it in Manage origins before saving.", + "The new Issuer URL is not in the organization's trusted origins list. Please add it in Manage origins before saving.", }); } } @@ -262,12 +285,21 @@ export const ssoRouter = createTRPCRouter({ addTrustedOrigin: enterpriseProcedure .input(z.object({ origin: z.string().min(1) })) .mutation(async ({ ctx, input }) => { + const ownerId = await getOrganizationOwnerId( + ctx.session.activeOrganizationId, + ); + if (!ownerId) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Organization owner not found", + }); + } const normalized = normalizeTrustedOrigin(input.origin); - const currentUser = await db.query.user.findFirst({ - where: eq(user.id, ctx.session.userId), + const ownerUser = await db.query.user.findFirst({ + where: eq(user.id, ownerId), columns: { trustedOrigins: true }, }); - const existing = currentUser?.trustedOrigins || []; + const existing = ownerUser?.trustedOrigins || []; if (existing.some((o) => o.toLowerCase() === normalized.toLowerCase())) { return { success: true }; } @@ -275,25 +307,34 @@ export const ssoRouter = createTRPCRouter({ await db .update(user) .set({ trustedOrigins: next }) - .where(eq(user.id, ctx.session.userId)); + .where(eq(user.id, ownerId)); return { success: true }; }), removeTrustedOrigin: enterpriseProcedure .input(z.object({ origin: z.string().min(1) })) .mutation(async ({ ctx, input }) => { + const ownerId = await getOrganizationOwnerId( + ctx.session.activeOrganizationId, + ); + if (!ownerId) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Organization owner not found", + }); + } const normalized = normalizeTrustedOrigin(input.origin); - const currentUser = await db.query.user.findFirst({ - where: eq(user.id, ctx.session.userId), + const ownerUser = await db.query.user.findFirst({ + where: eq(user.id, ownerId), columns: { trustedOrigins: true }, }); - const existing = currentUser?.trustedOrigins || []; + const existing = ownerUser?.trustedOrigins || []; const next = existing.filter( (o) => o.toLowerCase() !== normalized.toLowerCase(), ); await db .update(user) .set({ trustedOrigins: next }) - .where(eq(user.id, ctx.session.userId)); + .where(eq(user.id, ownerId)); return { success: true }; }), updateTrustedOrigin: enterpriseProcedure @@ -304,20 +345,29 @@ export const ssoRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { + const ownerId = await getOrganizationOwnerId( + ctx.session.activeOrganizationId, + ); + if (!ownerId) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Organization owner not found", + }); + } const oldNorm = normalizeTrustedOrigin(input.oldOrigin); const newNorm = normalizeTrustedOrigin(input.newOrigin); - const currentUser = await db.query.user.findFirst({ - where: eq(user.id, ctx.session.userId), + const ownerUser = await db.query.user.findFirst({ + where: eq(user.id, ownerId), columns: { trustedOrigins: true }, }); - const existing = currentUser?.trustedOrigins || []; + const existing = ownerUser?.trustedOrigins || []; const next = existing.map((o) => o.toLowerCase() === oldNorm.toLowerCase() ? newNorm : o, ); await db .update(user) .set({ trustedOrigins: next }) - .where(eq(user.id, ctx.session.userId)); + .where(eq(user.id, ownerId)); return { success: true }; }), }); diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index 3f217ceed..afab315e2 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -118,6 +118,14 @@ export const userRouter = createTRPCRouter({ return memberResult; }), + getTrustedOrigins: protectedProcedure.query(async ({ ctx }) => { + const memberResult = await db.query.member.findFirst({ + where: and( + eq(member.organizationId, ctx.session?.activeOrganizationId || ""), + ), + }); + return memberResult?.user?.trustedOrigins ?? []; + }), haveRootAccess: protectedProcedure.query(async ({ ctx }) => { if (!IS_CLOUD) { return false; diff --git a/apps/dokploy/server/api/trpc.ts b/apps/dokploy/server/api/trpc.ts index 51f8cdbee..084d529c7 100644 --- a/apps/dokploy/server/api/trpc.ts +++ b/apps/dokploy/server/api/trpc.ts @@ -7,6 +7,7 @@ * need to use are documented accordingly near the end. */ +import { hasValidLicense } from "@dokploy/server/index"; import { validateRequest } from "@dokploy/server/lib/auth"; import type { OpenApiMeta } from "@dokploy/trpc-openapi"; import { initTRPC, TRPCError } from "@trpc/server"; @@ -239,10 +240,11 @@ export const enterpriseProcedure = t.procedure.use(async ({ ctx, next }) => { throw new TRPCError({ code: "UNAUTHORIZED" }); } - if ( - !ctx.user?.enableEnterpriseFeatures || - !ctx.user.isValidEnterpriseLicense - ) { + const hasValidLicenseResult = await hasValidLicense( + ctx.session.activeOrganizationId, + ); + + if (!hasValidLicenseResult) { throw new TRPCError({ code: "FORBIDDEN", message: "Valid enterprise license required", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 7bfc5553b..c49a3005a 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -31,6 +31,7 @@ export * from "./services/port"; export * from "./services/postgres"; export * from "./services/preview-deployment"; export * from "./services/project"; +export * from "./services/proprietary/license-key"; export * from "./services/proprietary/sso"; export * from "./services/redirect"; export * from "./services/redis"; diff --git a/packages/server/src/services/proprietary/license-key.ts b/packages/server/src/services/proprietary/license-key.ts new file mode 100644 index 000000000..8cfa51406 --- /dev/null +++ b/packages/server/src/services/proprietary/license-key.ts @@ -0,0 +1,29 @@ +import { db } from "@dokploy/server/db"; +import { organization, user } from "@dokploy/server/db/schema"; +import { eq } from "drizzle-orm"; + +export const hasValidLicense = async (organizationId: string) => { + // we need to find the owner of the organization + const organizationResult = await db.query.organization.findFirst({ + where: eq(organization.id, organizationId), + columns: { ownerId: true }, + }); + + if (!organizationResult) { + return false; + } + + const ownerId = organizationResult?.ownerId; + + const currentUser = await db.query.user.findFirst({ + where: eq(user.id, ownerId), + columns: { + enableEnterpriseFeatures: true, + isValidEnterpriseLicense: true, + }, + }); + return !!( + currentUser?.enableEnterpriseFeatures && + currentUser?.isValidEnterpriseLicense + ); +}; diff --git a/packages/server/src/services/proprietary/sso.ts b/packages/server/src/services/proprietary/sso.ts index 85caf600c..352a042e0 100644 --- a/packages/server/src/services/proprietary/sso.ts +++ b/packages/server/src/services/proprietary/sso.ts @@ -1,4 +1,6 @@ import { db } from "@dokploy/server/db"; +import { organization } from "@dokploy/server/db/schema"; +import { eq } from "drizzle-orm"; export const getSSOProviders = async () => { const providers = await db.query.ssoProvider.findMany({ @@ -33,3 +35,12 @@ export const normalizeTrustedOrigin = (value: string): string => { // e.g. "https://example.com/" -> "https://example.com" return value.trim().replace(/\/+$/, ""); }; + +export const getOrganizationOwnerId = async (organizationId: string) => { + const org = await db.query.organization.findFirst({ + where: eq(organization.id, organizationId), + columns: { ownerId: true }, + }); + if (!org) return null; + return org.ownerId; +}; From be35709cea28a62f26b27d6c0cb2c42c3525f827 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 18 Feb 2026 01:35:57 -0600 Subject: [PATCH 2/5] fix(auth): remove callback URL from email sign-in process on home page --- apps/dokploy/pages/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/dokploy/pages/index.tsx b/apps/dokploy/pages/index.tsx index a778f50f4..d2ea83297 100644 --- a/apps/dokploy/pages/index.tsx +++ b/apps/dokploy/pages/index.tsx @@ -81,7 +81,6 @@ export default function Home({ IS_CLOUD }: Props) { const { data, error } = await authClient.signIn.email({ email: values.email, password: values.password, - callbackURL: "/dashboard/projects", }); if (error) { From 4e8d37bff77dad376d5a3d03e3b3e1a836b4bcbc Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 18 Feb 2026 01:38:04 -0600 Subject: [PATCH 3/5] refactor(user): remove getTrustedOrigins query from user router - Eliminated the getTrustedOrigins query from the user router to streamline the API and improve code maintainability. --- apps/dokploy/server/api/routers/user.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index afab315e2..3f217ceed 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -118,14 +118,6 @@ export const userRouter = createTRPCRouter({ return memberResult; }), - getTrustedOrigins: protectedProcedure.query(async ({ ctx }) => { - const memberResult = await db.query.member.findFirst({ - where: and( - eq(member.organizationId, ctx.session?.activeOrganizationId || ""), - ), - }); - return memberResult?.user?.trustedOrigins ?? []; - }), haveRootAccess: protectedProcedure.query(async ({ ctx }) => { if (!IS_CLOUD) { return false; From 605931861b09fa5e9ac2e9479df0203d88d86682 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Wed, 18 Feb 2026 01:39:59 -0600 Subject: [PATCH 4/5] Update packages/server/src/services/proprietary/license-key.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../server/src/services/proprietary/license-key.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/server/src/services/proprietary/license-key.ts b/packages/server/src/services/proprietary/license-key.ts index 8cfa51406..1feb8ee7d 100644 --- a/packages/server/src/services/proprietary/license-key.ts +++ b/packages/server/src/services/proprietary/license-key.ts @@ -3,18 +3,12 @@ import { organization, user } from "@dokploy/server/db/schema"; import { eq } from "drizzle-orm"; export const hasValidLicense = async (organizationId: string) => { - // we need to find the owner of the organization - const organizationResult = await db.query.organization.findFirst({ - where: eq(organization.id, organizationId), - columns: { ownerId: true }, - }); + const ownerId = await getOrganizationOwnerId(organizationId); - if (!organizationResult) { + if (!ownerId) { return false; } - const ownerId = organizationResult?.ownerId; - const currentUser = await db.query.user.findFirst({ where: eq(user.id, ownerId), columns: { From b9c62cc515703a6584970d8c5a0748639f0d3a37 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 18 Feb 2026 01:40:22 -0600 Subject: [PATCH 5/5] refactor(license-key): remove unused import and add organization owner ID retrieval - Removed the unused import of the organization schema. - Introduced a new import for the getOrganizationOwnerId function to enhance license validation logic. --- packages/server/src/services/proprietary/license-key.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/src/services/proprietary/license-key.ts b/packages/server/src/services/proprietary/license-key.ts index 1feb8ee7d..69976ca8c 100644 --- a/packages/server/src/services/proprietary/license-key.ts +++ b/packages/server/src/services/proprietary/license-key.ts @@ -1,6 +1,7 @@ import { db } from "@dokploy/server/db"; -import { organization, user } from "@dokploy/server/db/schema"; +import { user } from "@dokploy/server/db/schema"; import { eq } from "drizzle-orm"; +import { getOrganizationOwnerId } from "./sso"; export const hasValidLicense = async (organizationId: string) => { const ownerId = await getOrganizationOwnerId(organizationId);