diff --git a/apps/dokploy/components/proprietary/sso/register-oidc-dialog.tsx b/apps/dokploy/components/proprietary/sso/register-oidc-dialog.tsx index 20a829101..83e809f7d 100644 --- a/apps/dokploy/components/proprietary/sso/register-oidc-dialog.tsx +++ b/apps/dokploy/components/proprietary/sso/register-oidc-dialog.tsx @@ -1,8 +1,11 @@ "use client"; +import { zodResolver } from "@hookform/resolvers/zod"; import { Loader2 } from "lucide-react"; import { useState } from "react"; +import { useForm } from "react-hook-form"; import { toast } from "sonner"; +import { z } from "zod"; import { authClient } from "@/lib/auth-client"; import { Button } from "@/components/ui/button"; import { @@ -14,55 +17,67 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; const DEFAULT_SCOPES = ["openid", "email", "profile"]; +const oidcProviderSchema = z.object({ + providerId: z.string().min(1, "Provider ID is required").trim(), + issuer: z.string().min(1, "Issuer URL is required").url("Invalid URL").trim(), + domain: z.string().min(1, "Domain is required").trim(), + clientId: z.string().min(1, "Client ID is required").trim(), + clientSecret: z.string().min(1, "Client secret is required"), + scopes: z.string().optional(), +}); + +type OidcProviderForm = z.infer; + interface RegisterOidcDialogProps { children: React.ReactNode; onSuccess?: () => void; } +const formDefaultValues: OidcProviderForm = { + providerId: "", + issuer: "", + domain: "", + clientId: "", + clientSecret: "", + scopes: DEFAULT_SCOPES.join(" "), +}; + export function RegisterOidcDialog({ children, onSuccess }: RegisterOidcDialogProps) { const [open, setOpen] = useState(false); - const [isSubmitting, setIsSubmitting] = useState(false); - const [form, setForm] = useState({ - providerId: "", - issuer: "", - domain: "", - clientId: "", - clientSecret: "", - scopes: DEFAULT_SCOPES.join(" "), + + const form = useForm({ + resolver: zodResolver(oidcProviderSchema), + defaultValues: formDefaultValues, }); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if ( - !form.providerId.trim() || - !form.issuer.trim() || - !form.domain.trim() || - !form.clientId.trim() || - !form.clientSecret.trim() - ) { - toast.error("Please fill in all required fields"); - return; - } + const isSubmitting = form.formState.isSubmitting; - setIsSubmitting(true); + const onSubmit = async (data: OidcProviderForm) => { try { - const scopes = form.scopes - .trim() - .split(/\s+/) - .filter(Boolean); - const { data, error } = await authClient.sso.register({ - providerId: form.providerId.trim(), - issuer: form.issuer.trim(), - domain: form.domain.trim(), + const scopes = data.scopes?.trim() + ? data.scopes.trim().split(/\s+/).filter(Boolean) + : DEFAULT_SCOPES; + const { error } = await authClient.sso.register({ + providerId: data.providerId, + issuer: data.issuer, + domain: data.domain, oidcConfig: { - clientId: form.clientId.trim(), - clientSecret: form.clientSecret.trim(), - scopes: scopes.length > 0 ? scopes : DEFAULT_SCOPES, + clientId: data.clientId, + clientSecret: data.clientSecret, + scopes, pkce: true, }, }); @@ -73,22 +88,13 @@ export function RegisterOidcDialog({ children, onSuccess }: RegisterOidcDialogPr } toast.success("OIDC provider registered successfully"); - setForm({ - providerId: "", - issuer: "", - domain: "", - clientId: "", - clientSecret: "", - scopes: DEFAULT_SCOPES.join(" "), - }); + form.reset(formDefaultValues); setOpen(false); onSuccess?.(); } catch (err) { toast.error( err instanceof Error ? err.message : "Failed to register SSO provider", ); - } finally { - setIsSubmitting(false); } }; @@ -99,107 +105,136 @@ export function RegisterOidcDialog({ children, onSuccess }: RegisterOidcDialogPr Register OIDC provider - Add an OpenID Connect (OIDC) identity provider. Discovery will - fill endpoints from the issuer URL when possible. + Add any OIDC-compliant identity provider (e.g. Okta, Azure AD, + Google Workspace, Auth0, Keycloak). Discovery will fill endpoints + from the issuer URL when possible. -
-
- - - setForm((f) => ({ ...f, providerId: e.target.value })) - } - /> -

- Unique identifier; used in callback URL path. -

-
-
- - - setForm((f) => ({ ...f, issuer: e.target.value })) - } - /> -

- Discovery document is fetched from{" "} - - {"{issuer}"}/.well-known/openid-configuration - -

-
-
- - - setForm((f) => ({ ...f, domain: e.target.value })) - } - /> -

- Email domain(s) that use this provider (e.g. for sign-in by email). -

-
-
- - - setForm((f) => ({ ...f, clientId: e.target.value })) - } - /> -
-
- - - setForm((f) => ({ ...f, clientSecret: e.target.value })) - } - /> -
-
- - - setForm((f) => ({ ...f, scopes: e.target.value })) - } - /> -
- - - - -
+ /> + ( + + Issuer URL + + + + + Discovery document is fetched from{" "} + + {"{issuer}"}/.well-known/openid-configuration + + + + + )} + /> + ( + + Domain + + + + + Email domain(s) that use this provider (e.g. for sign-in by + email). + + + + )} + /> + ( + + Client ID + + + + + + )} + /> + ( + + Client secret + + + + + + )} + /> + ( + + Scopes (optional) + + + + + + )} + /> + + + + + + ); diff --git a/apps/dokploy/components/proprietary/sso/register-saml-dialog.tsx b/apps/dokploy/components/proprietary/sso/register-saml-dialog.tsx new file mode 100644 index 000000000..5c6a7f7cf --- /dev/null +++ b/apps/dokploy/components/proprietary/sso/register-saml-dialog.tsx @@ -0,0 +1,262 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Loader2 } from "lucide-react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { authClient } from "@/lib/auth-client"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; + +const samlProviderSchema = z.object({ + providerId: z.string().min(1, "Provider ID is required").trim(), + issuer: z.string().min(1, "Issuer URL is required").url("Invalid URL").trim(), + domain: z.string().min(1, "Domain is required").trim(), + entryPoint: z + .string() + .min(1, "IdP SSO URL is required") + .url("Invalid URL") + .trim(), + cert: z.string().min(1, "IdP signing certificate is required"), + callbackUrl: z + .string() + .min(1, "Callback URL is required") + .url("Invalid URL") + .trim(), + audience: z.string().min(1, "Audience (Entity ID) is required").trim(), +}); + +type SamlProviderForm = z.infer; + +interface RegisterSamlDialogProps { + children: React.ReactNode; + onSuccess?: () => void; +} + +const formDefaultValues: SamlProviderForm = { + providerId: "", + issuer: "", + domain: "", + entryPoint: "", + cert: "", + callbackUrl: "", + audience: "", +}; + +export function RegisterSamlDialog({ children, onSuccess }: RegisterSamlDialogProps) { + const [open, setOpen] = useState(false); + + const form = useForm({ + resolver: zodResolver(samlProviderSchema), + defaultValues: formDefaultValues, + }); + + const isSubmitting = form.formState.isSubmitting; + + const onSubmit = async (data: SamlProviderForm) => { + try { + const { error } = await authClient.sso.register({ + providerId: data.providerId, + issuer: data.issuer, + domain: data.domain, + samlConfig: { + entryPoint: data.entryPoint, + cert: data.cert, + callbackUrl: data.callbackUrl, + audience: data.audience, + wantAssertionsSigned: true, + signatureAlgorithm: "sha256", + digestAlgorithm: "sha256", + }, + }); + + if (error) { + toast.error(error.message ?? "Failed to register SAML provider"); + return; + } + + toast.success("SAML provider registered successfully"); + form.reset(formDefaultValues); + setOpen(false); + onSuccess?.(); + } catch (err) { + toast.error( + err instanceof Error ? err.message : "Failed to register SAML provider", + ); + } + }; + + return ( + + {children} + + + Register SAML provider + + Add a SAML 2.0 identity provider (e.g. Okta SAML, Azure AD SAML, + OneLogin). You need the IdP's SSO URL and signing certificate. + + +
+ + ( + + Provider ID + + + + + + )} + /> + ( + + Issuer URL + + + + + + )} + /> + ( + + Domain + + + + + + )} + /> + ( + + IdP SSO URL (Entry point) + + + + + Single Sign-On URL from your IdP's SAML setup. + + + + )} + /> + ( + + IdP signing certificate (X.509) + +