import { useState, useEffect, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/button"; import { Input } from "@/components/input"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/dialog"; import { toast } from "sonner"; import type { SSOProvider, SSOProviderType } from "@/types/index"; import type { OIDCProviderConfig, LDAPProviderConfig } from "@/types/index"; import { createSSOProvider, updateSSOProvider } from "@/api/sso-provider-api"; type ApiErrorLike = { response?: { data?: { error?: string } }; message?: string; }; function apiErrorMsg(error: unknown, fallback: string) { const err = error as ApiErrorLike; return err.response?.data?.error || err.message || fallback; } const PROVIDER_TYPE_OPTIONS: { value: SSOProviderType; label: string }[] = [ { value: "oidc", label: "OIDC" }, { value: "github", label: "GitHub" }, { value: "google", label: "Google" }, { value: "ldap", label: "LDAP" }, ]; type SSOProviderDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; provider: SSOProvider | null; onSaved: (provider: SSOProvider) => void; }; function emptyOidc(): OIDCProviderConfig { return { client_id: "", client_secret: "", issuer_url: "", authorization_url: "", token_url: "", userinfo_url: "", identifier_path: "sub", name_path: "name", scopes: "openid email profile", allowed_users: "", admin_group: "", group_claim: "", }; } function emptyLdap(): LDAPProviderConfig { return { host: "", port: 389, useTLS: false, bindDN: "", bindPassword: "", userSearchBase: "", userSearchFilter: "(uid={{username}})", usernameAttribute: "uid", displayNameAttribute: "cn", groupSearchBase: "", adminGroup: "", allowedUsers: "", }; } type OIDCFields = { client_id: string; client_secret: string; issuer_url: string; authorization_url: string; token_url: string; userinfo_url: string; identifier_path: string; name_path: string; scopes: string; allowed_users: string; admin_group: string; group_claim: string; }; type LDAPFields = { host: string; port: string; useTLS: boolean; bindDN: string; bindPassword: string; userSearchBase: string; userSearchFilter: string; usernameAttribute: string; displayNameAttribute: string; groupSearchBase: string; adminGroup: string; allowedUsers: string; }; export function SSOProviderDialog({ open, onOpenChange, provider, onSaved, }: SSOProviderDialogProps) { const { t } = useTranslation(); const isEdit = provider != null; const [name, setName] = useState(""); const [type, setType] = useState("oidc"); const [enabled, setEnabled] = useState(true); const [saving, setSaving] = useState(false); const [oidc, setOidc] = useState( () => ({ ...emptyOidc() }) as OIDCFields, ); const [ldap, setLdap] = useState(() => { const d = emptyLdap(); return { ...d, port: String(d.port) }; }); useEffect(() => { if (!open) return; if (provider) { setName(provider.name); setType(provider.type); setEnabled(provider.enabled); let config: Record = {}; if (typeof provider.config === "string") { try { config = JSON.parse(provider.config); } catch { /* */ } } else if (provider.config && typeof provider.config === "object") { config = provider.config as Record; } if (provider.type === "ldap") { const d = emptyLdap(); setLdap({ host: (config.host as string) ?? d.host, port: String((config.port as number) ?? d.port), useTLS: (config.useTLS as boolean) ?? d.useTLS, bindDN: (config.bindDN as string) ?? d.bindDN, bindPassword: (config.bindPassword as string) ?? "", userSearchBase: (config.userSearchBase as string) ?? d.userSearchBase, userSearchFilter: (config.userSearchFilter as string) ?? d.userSearchFilter, usernameAttribute: (config.usernameAttribute as string) ?? d.usernameAttribute, displayNameAttribute: (config.displayNameAttribute as string) ?? d.displayNameAttribute, groupSearchBase: (config.groupSearchBase as string) ?? "", adminGroup: (config.adminGroup as string) ?? "", allowedUsers: (config.allowedUsers as string) ?? "", }); } else { const d = emptyOidc(); setOidc({ client_id: (config.client_id as string) ?? d.client_id, client_secret: (config.client_secret as string) ?? "", issuer_url: (config.issuer_url as string) ?? d.issuer_url, authorization_url: (config.authorization_url as string) ?? d.authorization_url, token_url: (config.token_url as string) ?? d.token_url, userinfo_url: (config.userinfo_url as string) ?? d.userinfo_url, identifier_path: (config.identifier_path as string) ?? d.identifier_path, name_path: (config.name_path as string) ?? d.name_path, scopes: (config.scopes as string) ?? d.scopes, allowed_users: (config.allowed_users as string) ?? d.allowed_users, admin_group: (config.admin_group as string) ?? d.admin_group, group_claim: (config.group_claim as string) ?? "", }); } } else { setName(""); setType("oidc"); setEnabled(true); setOidc({ ...emptyOidc() } as OIDCFields); const d = emptyLdap(); setLdap({ ...d, port: String(d.port) }); } }, [open, provider]); function setOidcField( field: K, value: OIDCFields[K], ) { setOidc((prev) => ({ ...prev, [field]: value })); } function setLdapField( field: K, value: LDAPFields[K], ) { setLdap((prev) => ({ ...prev, [field]: value })); } function buildConfig(): Record { if (type === "ldap") { return { host: ldap.host, port: parseInt(ldap.port, 10) || 389, useTLS: ldap.useTLS, bindDN: ldap.bindDN, bindPassword: ldap.bindPassword, userSearchBase: ldap.userSearchBase, userSearchFilter: ldap.userSearchFilter, usernameAttribute: ldap.usernameAttribute || "uid", displayNameAttribute: ldap.displayNameAttribute || "cn", groupSearchBase: ldap.groupSearchBase || undefined, adminGroup: ldap.adminGroup || undefined, allowedUsers: ldap.allowedUsers || undefined, }; } return { client_id: oidc.client_id, client_secret: oidc.client_secret, issuer_url: oidc.issuer_url, authorization_url: oidc.authorization_url, token_url: oidc.token_url, userinfo_url: oidc.userinfo_url || undefined, identifier_path: oidc.identifier_path, name_path: oidc.name_path, scopes: oidc.scopes, allowed_users: oidc.allowed_users || undefined, admin_group: oidc.admin_group || undefined, group_claim: oidc.group_claim || undefined, }; } async function handleSave() { if (!name.trim()) { toast.error( t("admin.ssoProviderName") + " " + t("common.required").toLowerCase(), ); return; } setSaving(true); try { const data = { name: name.trim(), type, enabled, displayOrder: 0, config: buildConfig(), }; let saved: SSOProvider; if (isEdit && provider) { saved = await updateSSOProvider(provider.id, data); } else { saved = await createSSOProvider(data); } toast.success(t("common.saved")); onSaved(saved); onOpenChange(false); } catch (err) { toast.error(apiErrorMsg(err, t("common.saveFailed"))); } finally { setSaving(false); } } const isGithubOrGoogle = type === "github" || type === "google"; return ( {isEdit ? t("admin.ssoEditProvider") : t("admin.ssoAddProvider")}
setName(e.target.value)} placeholder="My SSO Provider" className="text-xs" />
{!isEdit && (
)}
{type === "ldap" ? ( ) : ( )}
); } function Field({ label, required, children, }: { label: string; required?: boolean; children: ReactNode; }) { return (
{children}
); } function OIDCConfigFields({ oidc, setOidcField, type, simplified, t, }: { oidc: OIDCFields; setOidcField: (k: K, v: OIDCFields[K]) => void; type: SSOProviderType; simplified: boolean; t: (key: string) => string; }) { const githubAuthUrl = "https://github.com/login/oauth/authorize"; const googleAuthUrl = "https://accounts.google.com/o/oauth2/v2/auth"; const docsHref = simplified ? "https://docs.termix.site/features/authentication/github-google" : "https://docs.termix.site/features/authentication/oidc"; return (
{t("admin.ssoProviderDocsLink")} setOidcField("client_id", e.target.value)} placeholder="your-client-id" className="text-xs" /> setOidcField("client_secret", e.target.value)} placeholder="your-client-secret" className="text-xs" /> {simplified ? (
{type === "github" ? `Authorization URL: ${githubAuthUrl}` : `Authorization URL: ${googleAuthUrl}`}
) : ( <> setOidcField("issuer_url", e.target.value)} placeholder="https://provider" className="text-xs" /> setOidcField("authorization_url", e.target.value) } placeholder="https://provider/oauth2/auth" className="text-xs" /> setOidcField("token_url", e.target.value)} placeholder="https://provider/oauth2/token" className="text-xs" /> setOidcField("identifier_path", e.target.value)} placeholder="sub" className="text-xs" /> setOidcField("name_path", e.target.value)} placeholder="name" className="text-xs" /> setOidcField("scopes", e.target.value)} placeholder="openid email profile" className="text-xs" /> setOidcField("userinfo_url", e.target.value)} placeholder="https://provider/oauth2/userinfo" className="text-xs" /> setOidcField("group_claim", e.target.value)} placeholder="groups" className="text-xs" /> )} {t("admin.oidcAllowedUsersDesc")}