import { useCallback, useEffect, useState, type ReactNode } from "react"; import { Copy, Pencil, Plus, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Input, Select2, Switch, copyToClipboard, } from "@termix/plugin-sdk/ui"; import { usePluginApi, useTranslation } from "@termix/plugin-sdk/frontend"; import { createSsoApi, type SsoProvider, type SsoProviderInput, type SsoProviderType, } from "./sso-api"; const TYPE_LABELS: Record = { oidc: "OIDC", github: "GitHub", google: "Google", }; const AUTHORIZATION_URLS: Record<"github" | "google", string> = { github: "https://github.com/login/oauth/authorize", google: "https://accounts.google.com/o/oauth2/v2/auth", }; type Fields = { 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; ca_cert: string; }; const EMPTY_FIELDS: Fields = { 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: "", ca_cert: "", }; function errorMessage(error: unknown, fallback: string): string { const err = error as { response?: { data?: { error?: string } }; message?: string; }; return err.response?.data?.error || err.message || fallback; } const labelClass = "text-[10px] font-semibold text-muted-foreground uppercase tracking-widest"; const areaClass = "w-full px-2 py-1.5 text-xs bg-background border border-border text-foreground placeholder:text-muted-foreground resize-none outline-none focus:ring-1 focus:ring-ring font-mono"; function Field({ label, required, hint, children, }: { label: string; required?: boolean; hint?: string; children: ReactNode; }) { return (
{hint && ( {hint} )} {children}
); } function RedirectUri({ uri }: { uri: string }) { const { t } = useTranslation(); return (
{uri}
); } function ProviderDialog({ open, onOpenChange, provider, newRedirectUri, onSaved, }: { open: boolean; onOpenChange: (open: boolean) => void; provider: SsoProvider | null; newRedirectUri: string; onSaved: () => void; }) { const { t } = useTranslation(); const api = createSsoApi(usePluginApi()); const isEdit = provider !== null; const [name, setName] = useState(""); const [type, setType] = useState("oidc"); const [enabled, setEnabled] = useState(true); const [legacyCallback, setLegacyCallback] = useState(false); const [fields, setFields] = useState(EMPTY_FIELDS); const [saving, setSaving] = useState(false); useEffect(() => { if (!open) return; setName(provider?.name ?? ""); setType(provider?.type ?? "oidc"); setEnabled(provider?.enabled ?? true); setLegacyCallback(provider?.legacyCallback ?? false); const config = provider?.config ?? {}; const next = { ...EMPTY_FIELDS }; for (const key of Object.keys(next) as Array) { const value = config[key]; if (typeof value === "string") next[key] = value; } next.client_secret = ""; setFields(next); }, [open, provider]); const set = (key: keyof Fields) => (value: string) => setFields((prev) => ({ ...prev, [key]: value })); const simplified = type !== "oidc"; async function save() { if (!name.trim()) { toast.error(t("providers.nameRequired")); return; } const config: Record = {}; for (const [key, value] of Object.entries(fields)) { if (value.trim()) config[key] = value.trim(); } const input: SsoProviderInput = { name: name.trim(), type, enabled, config, ...(isEdit ? { legacyCallback } : {}), }; setSaving(true); try { if (provider) await api.update(provider.id, input); else await api.create(input); toast.success(t("providers.saved")); onSaved(); onOpenChange(false); } catch (error) { toast.error(errorMessage(error, t("providers.saveFailed"))); } finally { setSaving(false); } } const secretHint = provider?.hasClientSecret ? t("fields.secretKeep") : undefined; const redirectUri = legacyCallback ? (provider?.redirectUri ?? newRedirectUri) : newRedirectUri; return ( {isEdit ? t("providers.edit") : t("providers.add")}
setName(e.target.value)} placeholder={t("providers.namePlaceholder")} className="text-xs" /> {!isEdit && ( setType(e.target.value as SsoProviderType)} className="w-full px-2 py-1.5 text-xs bg-background border border-border text-foreground outline-none focus:ring-1 focus:ring-ring" > {(Object.keys(TYPE_LABELS) as SsoProviderType[]).map( (value) => ( ), )} )}
{t("providers.redirectUriDesc")} {isEdit && provider.legacyCallback && (
{t("providers.legacyCallbackDesc", { uri: newRedirectUri, })}
)}
{t("providers.docsLink")} set("client_id")(e.target.value)} placeholder="your-client-id" className="text-xs" /> set("client_secret")(e.target.value)} placeholder="your-client-secret" className="text-xs" /> {simplified ? ( {t("providers.authorizationUrl", { url: AUTHORIZATION_URLS[type as "github" | "google"], })} ) : ( <> set("issuer_url")(e.target.value)} placeholder="https://provider" className="text-xs" /> set("authorization_url")(e.target.value)} placeholder="https://provider/oauth2/auth" className="text-xs" /> set("token_url")(e.target.value)} placeholder="https://provider/oauth2/token" className="text-xs" /> set("identifier_path")(e.target.value)} placeholder="sub" className="text-xs" /> set("name_path")(e.target.value)} placeholder="name" className="text-xs" /> set("scopes")(e.target.value)} placeholder="openid email profile" className="text-xs" /> set("userinfo_url")(e.target.value)} placeholder="https://provider/oauth2/userinfo" className="text-xs" /> set("group_claim")(e.target.value)} placeholder="groups" className="text-xs" /> )}