Skip to content

Commit eeb7f00

Browse files
authored
Merge pull request #3680 from Dokploy/feat/add-trusted-origins-sso
Feat/add trusted origins sso
2 parents 744ebab + 1326d14 commit eeb7f00

File tree

3 files changed

+294
-12
lines changed

3 files changed

+294
-12
lines changed

apps/dokploy/components/proprietary/sso/sso-settings.tsx

Lines changed: 225 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
"use client";
22

3-
import { Eye, Loader2, LogIn, Trash2 } from "lucide-react";
3+
import {
4+
Eye,
5+
Loader2,
6+
LogIn,
7+
Pencil,
8+
Plus,
9+
Shield,
10+
Trash2,
11+
} from "lucide-react";
412
import { useEffect, useState } from "react";
513
import { toast } from "sonner";
614
import { DialogAction } from "@/components/shared/dialog-action";
@@ -21,6 +29,7 @@ import {
2129
DialogHeader,
2230
DialogTitle,
2331
} from "@/components/ui/dialog";
32+
import { Input } from "@/components/ui/input";
2433
import { api } from "@/utils/api";
2534
import { RegisterOidcDialog } from "./register-oidc-dialog";
2635
import { RegisterSamlDialog } from "./register-saml-dialog";
@@ -68,6 +77,10 @@ export const SSOSettings = () => {
6877
const [detailsProvider, setDetailsProvider] =
6978
useState<ProviderForDetails | null>(null);
7079
const [baseURL, setBaseURL] = useState("");
80+
const [manageOriginsOpen, setManageOriginsOpen] = useState(false);
81+
const [editingOrigin, setEditingOrigin] = useState<string | null>(null);
82+
const [editingValue, setEditingValue] = useState("");
83+
const [newOriginInput, setNewOriginInput] = useState("");
7184

7285
useEffect(() => {
7386
if (typeof window !== "undefined") {
@@ -76,20 +89,101 @@ export const SSOSettings = () => {
7689
}, []);
7790

7891
const { data: providers, isLoading } = api.sso.listProviders.useQuery();
92+
const { data: userData } = api.user.get.useQuery(undefined, {
93+
enabled: manageOriginsOpen,
94+
});
7995
const { mutateAsync: deleteProvider, isLoading: isDeleting } =
8096
api.sso.deleteProvider.useMutation();
97+
const { mutateAsync: addTrustedOrigin, isLoading: isAddingOrigin } =
98+
api.sso.addTrustedOrigin.useMutation();
99+
const { mutateAsync: removeTrustedOrigin, isLoading: isRemovingOrigin } =
100+
api.sso.removeTrustedOrigin.useMutation();
101+
const { mutateAsync: updateTrustedOrigin, isLoading: isUpdatingOrigin } =
102+
api.sso.updateTrustedOrigin.useMutation();
103+
104+
const trustedOrigins = userData?.user?.trustedOrigins ?? [];
105+
106+
const handleAddOrigin = async () => {
107+
const value = newOriginInput.trim();
108+
if (!value) return;
109+
try {
110+
await addTrustedOrigin({ origin: value });
111+
toast.success("Trusted origin added");
112+
setNewOriginInput("");
113+
await utils.user.get.invalidate();
114+
} catch (err) {
115+
toast.error(
116+
err instanceof Error ? err.message : "Failed to add trusted origin",
117+
);
118+
}
119+
};
120+
121+
const handleRemoveOrigin = async (origin: string) => {
122+
try {
123+
await removeTrustedOrigin({ origin });
124+
toast.success("Trusted origin removed");
125+
if (editingOrigin === origin) setEditingOrigin(null);
126+
await utils.user.get.invalidate();
127+
} catch (err) {
128+
toast.error(
129+
err instanceof Error ? err.message : "Failed to remove trusted origin",
130+
);
131+
}
132+
};
133+
134+
const handleStartEdit = (origin: string) => {
135+
setEditingOrigin(origin);
136+
setEditingValue(origin);
137+
};
138+
139+
const handleSaveEdit = async () => {
140+
if (editingOrigin == null || !editingValue.trim()) {
141+
setEditingOrigin(null);
142+
return;
143+
}
144+
try {
145+
await updateTrustedOrigin({
146+
oldOrigin: editingOrigin,
147+
newOrigin: editingValue.trim(),
148+
});
149+
toast.success("Trusted origin updated");
150+
setEditingOrigin(null);
151+
setEditingValue("");
152+
await utils.user.get.invalidate();
153+
} catch (err) {
154+
toast.error(
155+
err instanceof Error ? err.message : "Failed to update trusted origin",
156+
);
157+
}
158+
};
159+
160+
const handleCancelEdit = () => {
161+
setEditingOrigin(null);
162+
setEditingValue("");
163+
};
81164

82165
return (
83166
<div className="flex flex-col gap-4 rounded-lg border p-4">
84-
<div className="flex flex-col gap-2">
85-
<div className="flex items-center gap-2">
86-
<LogIn className="size-6 text-muted-foreground" />
87-
<CardTitle className="text-xl">Single Sign-On (SSO)</CardTitle>
167+
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
168+
<div className="flex flex-col gap-2">
169+
<div className="flex items-center gap-2">
170+
<LogIn className="size-6 text-muted-foreground" />
171+
<CardTitle className="text-xl">Single Sign-On (SSO)</CardTitle>
172+
</div>
173+
<CardDescription>
174+
Configure OIDC or SAML identity providers for enterprise sign-in.
175+
Users can sign in with their organization&apos;s IdP.
176+
</CardDescription>
88177
</div>
89-
<CardDescription>
90-
Configure OIDC or SAML identity providers for enterprise sign-in.
91-
Users can sign in with their organization&apos;s IdP.
92-
</CardDescription>
178+
<Button
179+
variant="outline"
180+
size="sm"
181+
onClick={() => setManageOriginsOpen(true)}
182+
className="shrink-0"
183+
>
184+
<Shield className="mr-2 size-4" />
185+
Manage origins
186+
</Button>
93187
</div>
94188

95189
{isLoading ? (
@@ -366,6 +460,128 @@ export const SSOSettings = () => {
366460
)}
367461
</DialogContent>
368462
</Dialog>
463+
464+
<Dialog open={manageOriginsOpen} onOpenChange={setManageOriginsOpen}>
465+
<DialogContent className="sm:max-w-[480px]">
466+
<DialogHeader>
467+
<DialogTitle className="flex items-center gap-2">
468+
<Shield className="size-5" />
469+
Trusted origins
470+
</DialogTitle>
471+
<DialogDescription>
472+
Manage allowed origins for SSO callbacks. Add, edit, or remove
473+
origins for your account.
474+
</DialogDescription>
475+
</DialogHeader>
476+
<div className="space-y-4 py-2">
477+
<div className="space-y-2">
478+
<span className="text-sm font-medium">Current origins</span>
479+
{trustedOrigins.length === 0 ? (
480+
<p className="rounded-md border border-dashed bg-muted/30 px-3 py-4 text-center text-sm text-muted-foreground">
481+
No trusted origins yet. Add one below.
482+
</p>
483+
) : (
484+
<ul className="flex flex-col gap-2">
485+
{trustedOrigins.map((origin) => (
486+
<li
487+
key={origin}
488+
className="flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-2"
489+
>
490+
{editingOrigin === origin ? (
491+
<>
492+
<Input
493+
value={editingValue}
494+
onChange={(e) => setEditingValue(e.target.value)}
495+
placeholder="https://..."
496+
className="flex-1 font-mono text-sm"
497+
autoFocus
498+
/>
499+
<Button
500+
size="sm"
501+
onClick={handleSaveEdit}
502+
disabled={!editingValue.trim() || isUpdatingOrigin}
503+
>
504+
Save
505+
</Button>
506+
<Button
507+
size="sm"
508+
variant="ghost"
509+
onClick={handleCancelEdit}
510+
>
511+
Cancel
512+
</Button>
513+
</>
514+
) : (
515+
<>
516+
<span className="flex-1 break-all font-mono text-sm">
517+
{origin}
518+
</span>
519+
<Button
520+
variant="ghost"
521+
size="icon"
522+
className="size-8 shrink-0"
523+
onClick={() => handleStartEdit(origin)}
524+
>
525+
<Pencil className="size-3.5" />
526+
</Button>
527+
<DialogAction
528+
title="Remove trusted origin"
529+
description={`Remove "${origin}" from trusted origins?`}
530+
type="destructive"
531+
onClick={async () => handleRemoveOrigin(origin)}
532+
>
533+
<Button
534+
variant="ghost"
535+
size="icon"
536+
className="size-8 shrink-0 text-destructive hover:text-destructive"
537+
disabled={isRemovingOrigin}
538+
>
539+
<Trash2 className="size-3.5" />
540+
</Button>
541+
</DialogAction>
542+
</>
543+
)}
544+
</li>
545+
))}
546+
</ul>
547+
)}
548+
</div>
549+
<div className="space-y-2">
550+
<span className="text-sm font-medium">Add trusted origin</span>
551+
<div className="flex gap-2">
552+
<Input
553+
value={newOriginInput}
554+
onChange={(e) => setNewOriginInput(e.target.value)}
555+
placeholder="https://example.com"
556+
className="font-mono text-sm"
557+
onKeyDown={(e) => {
558+
if (e.key === "Enter") {
559+
e.preventDefault();
560+
void handleAddOrigin();
561+
}
562+
}}
563+
/>
564+
<Button
565+
size="sm"
566+
onClick={handleAddOrigin}
567+
disabled={!newOriginInput.trim() || isAddingOrigin}
568+
>
569+
<Plus className="mr-1 size-4" />
570+
Add
571+
</Button>
572+
</div>
573+
</div>
574+
</div>
575+
<DialogFooter>
576+
<Button
577+
variant="outline"
578+
onClick={() => setManageOriginsOpen(false)}
579+
>
580+
Close
581+
</Button>
582+
</DialogFooter>
583+
</DialogContent>
584+
</Dialog>
369585
</div>
370586
);
371587
};

apps/dokploy/server/api/routers/proprietary/sso.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,65 @@ export const ssoRouter = createTRPCRouter({
177177
});
178178
return { success: true };
179179
}),
180+
addTrustedOrigin: enterpriseProcedure
181+
.input(z.object({ origin: z.string().min(1) }))
182+
.mutation(async ({ ctx, input }) => {
183+
const normalized = normalizeTrustedOrigin(input.origin);
184+
const currentUser = await db.query.user.findFirst({
185+
where: eq(user.id, ctx.session.userId),
186+
columns: { trustedOrigins: true },
187+
});
188+
const existing = currentUser?.trustedOrigins || [];
189+
if (existing.some((o) => o.toLowerCase() === normalized.toLowerCase())) {
190+
return { success: true };
191+
}
192+
const next = Array.from(new Set([...existing, normalized]));
193+
await db
194+
.update(user)
195+
.set({ trustedOrigins: next })
196+
.where(eq(user.id, ctx.session.userId));
197+
return { success: true };
198+
}),
199+
removeTrustedOrigin: enterpriseProcedure
200+
.input(z.object({ origin: z.string().min(1) }))
201+
.mutation(async ({ ctx, input }) => {
202+
const normalized = normalizeTrustedOrigin(input.origin);
203+
const currentUser = await db.query.user.findFirst({
204+
where: eq(user.id, ctx.session.userId),
205+
columns: { trustedOrigins: true },
206+
});
207+
const existing = currentUser?.trustedOrigins || [];
208+
const next = existing.filter(
209+
(o) => o.toLowerCase() !== normalized.toLowerCase(),
210+
);
211+
await db
212+
.update(user)
213+
.set({ trustedOrigins: next })
214+
.where(eq(user.id, ctx.session.userId));
215+
return { success: true };
216+
}),
217+
updateTrustedOrigin: enterpriseProcedure
218+
.input(
219+
z.object({
220+
oldOrigin: z.string().min(1),
221+
newOrigin: z.string().min(1),
222+
}),
223+
)
224+
.mutation(async ({ ctx, input }) => {
225+
const oldNorm = normalizeTrustedOrigin(input.oldOrigin);
226+
const newNorm = normalizeTrustedOrigin(input.newOrigin);
227+
const currentUser = await db.query.user.findFirst({
228+
where: eq(user.id, ctx.session.userId),
229+
columns: { trustedOrigins: true },
230+
});
231+
const existing = currentUser?.trustedOrigins || [];
232+
const next = existing.map((o) =>
233+
o.toLowerCase() === oldNorm.toLowerCase() ? newNorm : o,
234+
);
235+
await db
236+
.update(user)
237+
.set({ trustedOrigins: next })
238+
.where(eq(user.id, ctx.session.userId));
239+
return { success: true };
240+
}),
180241
});

apps/dokploy/server/api/routers/stripe.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,17 @@ export const stripeRouter = createTRPCRouter({
2727
const products = await stripe.products.list({
2828
expand: ["data.default_price"],
2929
active: true,
30-
ids: [PRODUCT_MONTHLY_ID, PRODUCT_ANNUAL_ID],
30+
});
31+
32+
const filteredProducts = products.data.filter((product) => {
33+
return (
34+
product.id === PRODUCT_MONTHLY_ID || product.id === PRODUCT_ANNUAL_ID
35+
);
3136
});
3237

3338
if (!stripeCustomerId) {
3439
return {
35-
products: products.data,
40+
products: filteredProducts,
3641
subscriptions: [],
3742
};
3843
}
@@ -44,7 +49,7 @@ export const stripeRouter = createTRPCRouter({
4449
});
4550

4651
return {
47-
products: products.data,
52+
products: filteredProducts,
4853
subscriptions: subscriptions.data,
4954
};
5055
}),

0 commit comments

Comments
 (0)