diff --git a/apps/dokploy/components/proprietary/sso/sso-settings.tsx b/apps/dokploy/components/proprietary/sso/sso-settings.tsx index 830745e01..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, 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 +29,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 +77,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 +89,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 +460,128 @@ 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..b7f15c9e5 100644 --- a/apps/dokploy/server/api/routers/proprietary/sso.ts +++ b/apps/dokploy/server/api/routers/proprietary/sso.ts @@ -177,4 +177,65 @@ 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 }; + }), }); 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, }; }),