diff --git a/apps/dokploy/server/api/routers/proprietary/sso.ts b/apps/dokploy/server/api/routers/proprietary/sso.ts index 4f11f86c5..040d36721 100644 --- a/apps/dokploy/server/api/routers/proprietary/sso.ts +++ b/apps/dokploy/server/api/routers/proprietary/sso.ts @@ -1,6 +1,8 @@ +import { normalizeTrustedOrigin } from "@dokploy/server"; import { IS_CLOUD } from "@dokploy/server/constants"; -import { member, ssoProvider } from "@dokploy/server/db/schema"; +import { member, ssoProvider, user } from "@dokploy/server/db/schema"; import { ssoProviderBodySchema } from "@dokploy/server/db/schema/sso"; +import { requestToHeaders } from "@dokploy/server/index"; import { auth } from "@dokploy/server/lib/auth"; import { TRPCError } from "@trpc/server"; import { and, asc, eq } from "drizzle-orm"; @@ -12,20 +14,6 @@ import { } from "@/server/api/trpc"; import { db } from "@/server/db"; -function requestToHeaders(req: { - headers?: Record; -}): Headers { - const headers = new Headers(); - if (req?.headers) { - for (const [key, value] of Object.entries(req.headers)) { - if (value !== undefined && key.toLowerCase() !== "host") { - headers.set(key, Array.isArray(value) ? value.join(", ") : value); - } - } - } - return headers; -} - export const ssoRouter = createTRPCRouter({ showSignInWithSSO: publicProcedure.query(async () => { if (IS_CLOUD) { @@ -73,6 +61,28 @@ export const ssoRouter = createTRPCRouter({ deleteProvider: enterpriseProcedure .input(z.object({ providerId: z.string().min(1) })) .mutation(async ({ ctx, input }) => { + // Obtener el provider antes de eliminarlo para obtener sus dominios + const providerToDelete = await db.query.ssoProvider.findFirst({ + where: and( + eq(ssoProvider.providerId, input.providerId), + eq(ssoProvider.organizationId, ctx.session.activeOrganizationId), + eq(ssoProvider.userId, ctx.session.userId), + ), + columns: { + id: true, + domain: true, + issuer: true, + }, + }); + + if (!providerToDelete) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "SSO provider not found or you do not have permission to delete it", + }); + } + const [deleted] = await db .delete(ssoProvider) .where( @@ -92,6 +102,24 @@ export const ssoRouter = createTRPCRouter({ }); } + const currentUser = await db.query.user.findFirst({ + where: eq(user.id, ctx.session.userId), + columns: { + trustedOrigins: true, + }, + }); + + if (currentUser?.trustedOrigins) { + const issuerOrigin = normalizeTrustedOrigin(providerToDelete.issuer); + const updatedOrigins = currentUser.trustedOrigins.filter( + (origin) => origin.toLowerCase() !== issuerOrigin.toLowerCase(), + ); + + await db + .update(user) + .set({ trustedOrigins: updatedOrigins }) + .where(eq(user.id, ctx.session.userId)); + } return { success: true }; }), register: enterpriseProcedure @@ -119,6 +147,26 @@ export const ssoRouter = createTRPCRouter({ } } const domain = input.domains.join(","); + const currentUser = await db.query.user.findFirst({ + where: eq(user.id, ctx.session.userId), + columns: { + trustedOrigins: true, + }, + }); + + const existingOrigins = currentUser?.trustedOrigins || []; + + const issuerOrigin = normalizeTrustedOrigin(input.issuer); + + const newOrigins = Array.from( + new Set([...existingOrigins, issuerOrigin]), + ); + + await db + .update(user) + .set({ trustedOrigins: newOrigins }) + .where(eq(user.id, ctx.session.userId)); + await auth.registerSSOProvider({ body: { ...input, diff --git a/packages/server/src/db/schema/user.ts b/packages/server/src/db/schema/user.ts index 9669d68d6..75e9ebf54 100644 --- a/packages/server/src/db/schema/user.ts +++ b/packages/server/src/db/schema/user.ts @@ -65,6 +65,7 @@ export const user = pgTable("user", { stripeCustomerId: text("stripeCustomerId"), stripeSubscriptionId: text("stripeSubscriptionId"), serversQuantity: integer("serversQuantity").notNull().default(0), + trustedOrigins: text("trustedOrigins").array(), }); export const usersRelations = relations(user, ({ one, many }) => ({ @@ -85,6 +86,8 @@ const createSchema = createInsertSchema(user, { isRegistered: z.boolean().optional(), }).omit({ role: true, + trustedOrigins: true, + isValidEnterpriseLicense: true, }); export const apiCreateUserInvitation = createSchema.pick({}).extend({ diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index 007be402e..1e2be4517 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -9,7 +9,7 @@ import { and, desc, eq } from "drizzle-orm"; import { IS_CLOUD } from "../constants"; import { db } from "../db"; import * as schema from "../db/schema"; -import { getUserByToken } from "../services/admin"; +import { getTrustedOrigins, getUserByToken } from "../services/admin"; import { getWebServerSettings, updateWebServerSettings, @@ -43,28 +43,24 @@ const { handler, api } = betterAuth({ logger: { disabled: process.env.NODE_ENV === "production", }, - ...(!IS_CLOUD && { - async trustedOrigins() { - const settings = await getWebServerSettings(); - if (!settings) { - return []; - } - return [ - ...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []), - ...(settings?.host ? [`https://${settings?.host}`] : []), - ...(process.env.NODE_ENV === "development" - ? [ - "http://localhost:3000", - "https://absolutely-handy-falcon.ngrok-free.app", - "https://dev-pee8hhc3qbjlqedb.us.auth0.com", - "https://trial-2804699.okta.com", - "https://login.microsoftonline.com", - "https://graph.microsoft.com", - ] - : []), - ]; - }, - }), + async trustedOrigins() { + const trustedOrigins = await getTrustedOrigins(); + if (IS_CLOUD) { + return trustedOrigins; + } + const settings = await getWebServerSettings(); + if (!settings) { + return []; + } + return [ + ...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []), + ...(settings?.host ? [`https://${settings?.host}`] : []), + ...(process.env.NODE_ENV === "development" + ? ["http://localhost:3000"] + : []), + ...trustedOrigins, + ]; + }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, diff --git a/packages/server/src/services/admin.ts b/packages/server/src/services/admin.ts index 323d0177e..510e1cae2 100644 --- a/packages/server/src/services/admin.ts +++ b/packages/server/src/services/admin.ts @@ -116,3 +116,18 @@ export const getDokployUrl = async () => { } return `http://${settings?.serverIp}:${process.env.PORT}`; }; + +export const getTrustedOrigins = async () => { + const members = await db.query.member.findMany({ + where: eq(member.role, "owner"), + with: { + user: true, + }, + }); + + const trustedOrigins = members.flatMap( + (member) => member.user.trustedOrigins || [], + ); + + return Array.from(new Set(trustedOrigins)); +}; diff --git a/packages/server/src/services/proprietary/sso.ts b/packages/server/src/services/proprietary/sso.ts index cc8d40394..85caf600c 100644 --- a/packages/server/src/services/proprietary/sso.ts +++ b/packages/server/src/services/proprietary/sso.ts @@ -13,3 +13,23 @@ export const getSSOProviders = async () => { }); return providers; }; + +export const requestToHeaders = (req: { + headers?: Record; +}): Headers => { + const headers = new Headers(); + if (req?.headers) { + for (const [key, value] of Object.entries(req.headers)) { + if (value !== undefined && key.toLowerCase() !== "host") { + headers.set(key, Array.isArray(value) ? value.join(", ") : value); + } + } + } + return headers; +}; + +export const normalizeTrustedOrigin = (value: string): string => { + // Keep it simple: trim and remove trailing slashes. + // e.g. "https://example.com/" -> "https://example.com" + return value.trim().replace(/\/+$/, ""); +};