"use client" import { useEffect, useMemo, useState } from "react" import { Copy, Loader2, Plus, RefreshCw, TestTube2, Trash2, } from "lucide-react" import { guideForProvider } from "@/components/admin/settings/guides/identity-provider-guides" import { TechBrandSelectLabel } from "@/components/admin/settings/tech-brand-select-label" import { useOrgSettingsStore } from "@/lib/admin-settings/org-settings-store" import type { IdentityProvider, IdentityProviderType, OAuthProviderPreset, } from "@/lib/admin-settings/org-settings-types" import { useIdentityProviderRedirectURI, useSyncIdentityProvider, useTestIdentityProvider, } from "@/lib/api/hooks/use-identity-provider-actions" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Sheet, SheetContent, SheetHeader, SheetTitle, } from "@/components/ui/sheet" import { Switch } from "@/components/ui/switch" import { Textarea } from "@/components/ui/textarea" import { toast } from "sonner" const OAUTH_PRESET_LABELS: Record = { google: "Google", github: "GitHub", linkedin: "LinkedIn", microsoft: "Microsoft", custom: "Autre / custom", } function splitList(value: string): string[] { return value .split(/[\n,]/) .map((item) => item.trim()) .filter(Boolean) } function joinList(values: string[] | undefined): string { return (values ?? []).join("\n") } function slugify(value: string): string { return value .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") } function emptyProvider(type: IdentityProviderType): IdentityProvider { const id = crypto.randomUUID() const base = { id, name: "", slug: "", type, enabled: true, sync_status: "pending" as const, allowed_email_domains: [], allowed_identities: [], allowed_organizations: [], default_groups: [], } if (type === "oauth") { return { ...base, oauth: { provider: "google", client_id: "", client_secret: "", scopes: "openid email profile", }, } } if (type === "saml") { return { ...base, saml: { metadata_url: "", entity_id: "", sso_url: "" } } } return { ...base, ldap: { server_uri: "", bind_dn: "", bind_password: "", base_dn: "", user_filter: "", start_tls: true, sync_users: false, }, } } function syncBadge(status: IdentityProvider["sync_status"]) { switch (status) { case "synced": return Synchronisé case "error": return Erreur sync default: return En attente } } export function IdentityProvidersPanel({ onRegisterBeforeSave, }: { onRegisterBeforeSave?: (fn: (() => void) | null) => void }) { const identityProviders = useOrgSettingsStore((s) => s.identityProviders) const setIdentityProviders = useOrgSettingsStore((s) => s.setIdentityProviders) const meta = useOrgSettingsStore((s) => s.meta) const [draft, setDraft] = useState(identityProviders) const [addOpen, setAddOpen] = useState(false) const [editIndex, setEditIndex] = useState(null) const [newType, setNewType] = useState("oauth") const testMutation = useTestIdentityProvider() const syncMutation = useSyncIdentityProvider() const redirectMutation = useIdentityProviderRedirectURI() useEffect(() => { setDraft(identityProviders) }, [identityProviders]) const redirectTemplate = meta?.effective.identity_providers?.oauth_redirect_template ?? "http://localhost/auth/source/oauth/callback/{slug}/" const editingProvider = editIndex != null ? draft.providers[editIndex] : null const guide = useMemo(() => { if (!editingProvider) return null return guideForProvider( editingProvider.type, editingProvider.oauth?.provider ) }, [editingProvider]) function updateProvider(index: number, patch: Partial) { setDraft((prev) => { const providers = [...prev.providers] providers[index] = { ...providers[index], ...patch } return { ...prev, providers } }) } function addProvider(type: IdentityProviderType) { const provider = emptyProvider(type) setDraft((prev) => ({ ...prev, providers: [...prev.providers, provider], })) setAddOpen(false) setEditIndex(draft.providers.length) } function removeProvider(index: number) { setDraft((prev) => ({ ...prev, providers: prev.providers.filter((_, i) => i !== index), })) if (editIndex === index) setEditIndex(null) } function providerSecrets(provider: IdentityProvider): Record { const idpSecrets = meta?.secrets?.identity_providers if (!idpSecrets || typeof idpSecrets !== "object") return {} const entry = (idpSecrets as Record>)[ provider.id ] return entry ?? {} } async function handleTest(provider: IdentityProvider) { try { await testMutation.mutateAsync(provider.id) toast.success("Configuration valide") } catch (error) { toast.error(error instanceof Error ? error.message : "Test échoué") } } async function handleSync(provider: IdentityProvider) { try { await syncMutation.mutateAsync(provider.id) toast.success("Synchronisation Authentik lancée") } catch { toast.error("Synchronisation échouée") } } async function copyRedirect(slug: string) { try { const res = await redirectMutation.mutateAsync(slug) await navigator.clipboard.writeText(res.redirect_uri) toast.success("URI de redirection copiée") } catch { const fallback = redirectTemplate.replace("{slug}", slug) await navigator.clipboard.writeText(fallback) toast.success("URI de redirection copiée") } } useEffect(() => { onRegisterBeforeSave?.(() => setIdentityProviders(draft)) return () => onRegisterBeforeSave?.(null) }, [draft, onRegisterBeforeSave, setIdentityProviders]) return ( <>
{draft.providers.length === 0 ? (

Aucun fournisseur. Ajoutez Google Workspace, Azure AD SAML, LDAP AD ou un OAuth custom.

) : (
{draft.providers.map((provider, index) => (
{provider.name || provider.slug || "Nouveau fournisseur"} {provider.type.toUpperCase()} {syncBadge(provider.sync_status)}

Slug : {provider.slug || "—"} {provider.sync_error ? ` · ${provider.sync_error}` : ""}

updateProvider(index, { enabled })} />
))}
)} Ajouter un fournisseur
!open && setEditIndex(null)}> {editingProvider && editIndex != null ? ( <> Configurer le fournisseur
{ const name = e.target.value updateProvider(editIndex, { name, slug: editingProvider.slug || slugify(name), }) }} />
updateProvider(editIndex, { slug: slugify(e.target.value) }) } />
{editingProvider.type === "oauth" ? (
updateProvider(editIndex, { oauth: { ...editingProvider.oauth!, client_id: e.target.value }, }) } />
updateProvider(editIndex, { oauth: { ...editingProvider.oauth!, client_secret: e.target.value, }, }) } />
updateProvider(editIndex, { oauth: { ...editingProvider.oauth!, scopes: e.target.value }, }) } />
{editingProvider.oauth?.provider === "custom" ? ( <>
updateProvider(editIndex, { oauth: { ...editingProvider.oauth!, authorization_url: e.target.value, }, }) } />
updateProvider(editIndex, { oauth: { ...editingProvider.oauth!, token_url: e.target.value, }, }) } />
updateProvider(editIndex, { oauth: { ...editingProvider.oauth!, profile_url: e.target.value, }, }) } />
) : null}
) : null} {editingProvider.type === "saml" ? (
updateProvider(editIndex, { saml: { ...editingProvider.saml!, metadata_url: e.target.value }, }) } />
updateProvider(editIndex, { saml: { ...editingProvider.saml!, entity_id: e.target.value }, }) } />
updateProvider(editIndex, { saml: { ...editingProvider.saml!, sso_url: e.target.value }, }) } />