From fe807ae2a6092ef6b939106e3eadae96efc938c6 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 10 Feb 2026 17:52:41 -0600 Subject: [PATCH 1/3] feat(sso): implement management for trusted origins in SSO settings - Added functionality to add, edit, and remove trusted origins for SSO callbacks. - Introduced new API mutations for managing trusted origins. - Enhanced the SSO settings UI to include a dialog for managing trusted origins, with appropriate state handling and user feedback via toast notifications. --- .../proprietary/sso/sso-settings.tsx | 230 +++++++++++++++++- .../server/api/routers/proprietary/sso.ts | 59 +++++ 2 files changed, 280 insertions(+), 9 deletions(-) diff --git a/apps/dokploy/components/proprietary/sso/sso-settings.tsx b/apps/dokploy/components/proprietary/sso/sso-settings.tsx index 830745e01..db5e934ec 100644 --- a/apps/dokploy/components/proprietary/sso/sso-settings.tsx +++ b/apps/dokploy/components/proprietary/sso/sso-settings.tsx @@ -1,6 +1,6 @@ "use client"; -import { Eye, Loader2, LogIn, Trash2 } from "lucide-react"; +import { Eye, Loader2, LogIn, Pencil, Plus, Shield, Trash2 } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; @@ -21,6 +21,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { RegisterOidcDialog } from "./register-oidc-dialog"; import { RegisterSamlDialog } from "./register-saml-dialog"; @@ -68,6 +69,10 @@ export const SSOSettings = () => { const [detailsProvider, setDetailsProvider] = useState(null); const [baseURL, setBaseURL] = useState(""); + const [manageOriginsOpen, setManageOriginsOpen] = useState(false); + const [editingOrigin, setEditingOrigin] = useState(null); + const [editingValue, setEditingValue] = useState(""); + const [newOriginInput, setNewOriginInput] = useState(""); useEffect(() => { if (typeof window !== "undefined") { @@ -76,20 +81,101 @@ export const SSOSettings = () => { }, []); const { data: providers, isLoading } = api.sso.listProviders.useQuery(); + const { data: userData } = api.user.get.useQuery(undefined, { + enabled: manageOriginsOpen, + }); const { mutateAsync: deleteProvider, isLoading: isDeleting } = api.sso.deleteProvider.useMutation(); + const { mutateAsync: addTrustedOrigin, isLoading: isAddingOrigin } = + api.sso.addTrustedOrigin.useMutation(); + const { mutateAsync: removeTrustedOrigin, isLoading: isRemovingOrigin } = + api.sso.removeTrustedOrigin.useMutation(); + const { mutateAsync: updateTrustedOrigin, isLoading: isUpdatingOrigin } = + api.sso.updateTrustedOrigin.useMutation(); + + const trustedOrigins = userData?.user?.trustedOrigins ?? []; + + const handleAddOrigin = async () => { + const value = newOriginInput.trim(); + if (!value) return; + try { + await addTrustedOrigin({ origin: value }); + toast.success("Trusted origin added"); + setNewOriginInput(""); + await utils.user.get.invalidate(); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to add trusted origin", + ); + } + }; + + const handleRemoveOrigin = async (origin: string) => { + try { + await removeTrustedOrigin({ origin }); + toast.success("Trusted origin removed"); + if (editingOrigin === origin) setEditingOrigin(null); + await utils.user.get.invalidate(); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to remove trusted origin", + ); + } + }; + + const handleStartEdit = (origin: string) => { + setEditingOrigin(origin); + setEditingValue(origin); + }; + + const handleSaveEdit = async () => { + if (editingOrigin == null || !editingValue.trim()) { + setEditingOrigin(null); + return; + } + try { + await updateTrustedOrigin({ + oldOrigin: editingOrigin, + newOrigin: editingValue.trim(), + }); + toast.success("Trusted origin updated"); + setEditingOrigin(null); + setEditingValue(""); + await utils.user.get.invalidate(); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to update trusted origin", + ); + } + }; + + const handleCancelEdit = () => { + setEditingOrigin(null); + setEditingValue(""); + }; return (
-
-
- - Single Sign-On (SSO) +
+
+
+ + Single Sign-On (SSO) +
+ + Configure OIDC or SAML identity providers for enterprise sign-in. + Users can sign in with their organization's IdP. +
- - Configure OIDC or SAML identity providers for enterprise sign-in. - Users can sign in with their organization's IdP. - +
{isLoading ? ( @@ -366,6 +452,132 @@ export const SSOSettings = () => { )} + + + + + + + Trusted origins + + + Manage allowed origins for SSO callbacks. Add, edit, or remove + origins for your account. + + +
+
+ Current origins + {trustedOrigins.length === 0 ? ( +

+ No trusted origins yet. Add one below. +

+ ) : ( +
    + {trustedOrigins.map((origin) => ( +
  • + {editingOrigin === origin ? ( + <> + setEditingValue(e.target.value)} + placeholder="https://..." + className="flex-1 font-mono text-sm" + autoFocus + /> + + + + ) : ( + <> + + {origin} + + + + handleRemoveOrigin(origin) + } + > + + + + )} +
  • + ))} +
+ )} +
+
+ Add trusted origin +
+ setNewOriginInput(e.target.value)} + placeholder="https://example.com" + className="font-mono text-sm" + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + void handleAddOrigin(); + } + }} + /> + +
+
+
+ + + +
+
); }; diff --git a/apps/dokploy/server/api/routers/proprietary/sso.ts b/apps/dokploy/server/api/routers/proprietary/sso.ts index 040d36721..e80a2139f 100644 --- a/apps/dokploy/server/api/routers/proprietary/sso.ts +++ b/apps/dokploy/server/api/routers/proprietary/sso.ts @@ -177,4 +177,63 @@ export const ssoRouter = createTRPCRouter({ }); return { success: true }; }), + addTrustedOrigin: enterpriseProcedure + .input(z.object({ origin: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + const normalized = normalizeTrustedOrigin(input.origin); + const currentUser = await db.query.user.findFirst({ + where: eq(user.id, ctx.session.userId), + columns: { trustedOrigins: true }, + }); + const existing = currentUser?.trustedOrigins || []; + if (existing.some((o) => o.toLowerCase() === normalized.toLowerCase())) { + return { success: true }; + } + const next = Array.from(new Set([...existing, normalized])); + await db + .update(user) + .set({ trustedOrigins: next }) + .where(eq(user.id, ctx.session.userId)); + return { success: true }; + }), + removeTrustedOrigin: enterpriseProcedure + .input(z.object({ origin: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + const normalized = normalizeTrustedOrigin(input.origin); + const currentUser = await db.query.user.findFirst({ + where: eq(user.id, ctx.session.userId), + columns: { trustedOrigins: true }, + }); + const existing = currentUser?.trustedOrigins || []; + const next = existing.filter((o) => o.toLowerCase() !== normalized.toLowerCase()); + await db + .update(user) + .set({ trustedOrigins: next }) + .where(eq(user.id, ctx.session.userId)); + return { success: true }; + }), + updateTrustedOrigin: enterpriseProcedure + .input( + z.object({ + oldOrigin: z.string().min(1), + newOrigin: z.string().min(1), + }), + ) + .mutation(async ({ ctx, input }) => { + const oldNorm = normalizeTrustedOrigin(input.oldOrigin); + const newNorm = normalizeTrustedOrigin(input.newOrigin); + const currentUser = await db.query.user.findFirst({ + where: eq(user.id, ctx.session.userId), + columns: { trustedOrigins: true }, + }); + const existing = currentUser?.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)); + return { success: true }; + }), }); From 59f843f8a01e0f3b448a5facaf37ba79bdb1ebda Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 10 Feb 2026 17:55:50 -0600 Subject: [PATCH 2/3] fix(stripe): filter products to include only monthly and annual subscriptions - Updated the Stripe API response to return only the monthly and annual subscription products. - Enhanced the product listing logic to filter out unnecessary products, improving data handling in the application. --- apps/dokploy/server/api/routers/stripe.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/server/api/routers/stripe.ts b/apps/dokploy/server/api/routers/stripe.ts index 412c9e3e8..963ec8c5b 100644 --- a/apps/dokploy/server/api/routers/stripe.ts +++ b/apps/dokploy/server/api/routers/stripe.ts @@ -27,12 +27,17 @@ export const stripeRouter = createTRPCRouter({ const products = await stripe.products.list({ expand: ["data.default_price"], active: true, - ids: [PRODUCT_MONTHLY_ID, PRODUCT_ANNUAL_ID], + }); + + const filteredProducts = products.data.filter((product) => { + return ( + product.id === PRODUCT_MONTHLY_ID || product.id === PRODUCT_ANNUAL_ID + ); }); if (!stripeCustomerId) { return { - products: products.data, + products: filteredProducts, subscriptions: [], }; } @@ -44,7 +49,7 @@ export const stripeRouter = createTRPCRouter({ }); return { - products: products.data, + products: filteredProducts, subscriptions: subscriptions.data, }; }), From 1326d14a001ba213d31e7d349700163c9ed638c5 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:59:10 +0000 Subject: [PATCH 3/3] [autofix.ci] apply automated fixes --- .../proprietary/sso/sso-settings.tsx | 18 +++++++++++------- .../server/api/routers/proprietary/sso.ts | 4 +++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/dokploy/components/proprietary/sso/sso-settings.tsx b/apps/dokploy/components/proprietary/sso/sso-settings.tsx index db5e934ec..842c5d87d 100644 --- a/apps/dokploy/components/proprietary/sso/sso-settings.tsx +++ b/apps/dokploy/components/proprietary/sso/sso-settings.tsx @@ -1,6 +1,14 @@ "use client"; -import { Eye, Loader2, LogIn, Pencil, Plus, Shield, Trash2 } from "lucide-react"; +import { + Eye, + Loader2, + LogIn, + Pencil, + Plus, + Shield, + Trash2, +} from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; @@ -491,9 +499,7 @@ export const SSOSettings = () => { @@ -522,9 +528,7 @@ export const SSOSettings = () => { title="Remove trusted origin" description={`Remove "${origin}" from trusted origins?`} type="destructive" - onClick={async () => - handleRemoveOrigin(origin) - } + onClick={async () => handleRemoveOrigin(origin)} >