"use client" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { Loader2 } from "lucide-react" import { AuthCard } from "@/components/auth/auth-card" import { AuthConnectButton } from "@/components/auth/auth-connect-button" import { FlowChallengeForm } from "@/components/auth/flow-challenge-form" import { AUTH_FLOW_SLUGS, isAuthenticationFlow, type AuthFlowSlug, } from "@/lib/auth/auth-flow-slugs" import { fetchEmbeddedAuthContext, flowComponent, flowRedirectUrl, flowTitle, flowValidationErrors, isOAuthAuthorizeRedirect, isRecoveryEmailSent, respondAuthFlow, respondDirectFlow, startAuthFlow, startDirectFlow, type FlowChallenge, } from "@/lib/auth/flow-api" type AuthFlowPageProps = { slug: AuthFlowSlug flowQuery?: string defaultTitle: string defaultDescription: string successTitle: string successDescription: string successActionLabel: string successHref: string /** Full document navigation required (OIDC login routes). */ successExternal?: boolean /** After embedded auth flow, bridge to OIDC session via BFF /flows/complete. */ bridgeAuthentication?: boolean returnTo?: string initialError?: string | null footer: React.ReactNode onSuccess?: () => void } function decodeAuthError(value: string | null | undefined): string | null { if (!value) return null try { return decodeURIComponent(value) } catch { return value } } export function AuthFlowPage({ slug, flowQuery, defaultTitle, defaultDescription, successTitle, successDescription, successActionLabel, successHref, successExternal = false, bridgeAuthentication = false, returnTo = "/mail/inbox", initialError = null, footer, onSuccess, }: AuthFlowPageProps) { const [challenge, setChallenge] = useState(null) const [loading, setLoading] = useState(true) const [submitting, setSubmitting] = useState(false) const [bridging, setBridging] = useState(false) const [flowError, setFlowError] = useState(null) const [done, setDone] = useState(false) const [denied, setDenied] = useState(false) const flowQueryRef = useRef(flowQuery) const bootstrappedRef = useRef(false) const bridgedRef = useRef(false) // Direct mode: drive Authentik's flow executor in the browser so the browser holds the session, // then navigate to the authorize URL to obtain the OIDC code. const directMode = bridgeAuthentication && isAuthenticationFlow(slug) const executorBaseRef = useRef(null) const authorizeUrlRef = useRef(null) const startedRef = useRef(false) const redirectError = decodeAuthError(initialError) const finishAuthentication = useCallback( (finalChallenge?: FlowChallenge | null) => { if (!directMode || bridgedRef.current) { onSuccess?.() return } bridgedRef.current = true setBridging(true) const target = flowRedirectUrl(finalChallenge ?? challenge) if (target && isOAuthAuthorizeRedirect(target)) { window.location.href = target return } if (authorizeUrlRef.current) { window.location.href = authorizeUrlRef.current return } // No authorize continuation available — surface an error rather than silently failing. setBridging(false) bridgedRef.current = false setFlowError("Impossible de finaliser la connexion (authorize introuvable)") }, [challenge, directMode, onSuccess] ) useEffect(() => { if (bootstrappedRef.current) return bootstrappedRef.current = true void (async () => { setLoading(true) try { let step: Awaited> if (directMode) { const ctx = await fetchEmbeddedAuthContext(returnTo) executorBaseRef.current = ctx.executorBase authorizeUrlRef.current = ctx.authorizeUrl flowQueryRef.current = ctx.flowQuery step = await startDirectFlow(ctx.executorBase, slug, ctx.flowQuery) } else { step = await startAuthFlow(slug, flowQueryRef.current ?? flowQuery) } startedRef.current = true setChallenge(step.challenge) setDone(step.done) setDenied(step.denied) if (step.done && !step.denied) { if (directMode) { finishAuthentication(step.challenge) return } onSuccess?.() } } catch (err) { setFlowError( err instanceof Error ? err.message : "Impossible de démarrer le parcours" ) } finally { setLoading(false) } })() // Intentionally once per mount — avoid re-start loop from challenge deps. // eslint-disable-next-line react-hooks/exhaustive-deps -- bootstrap runs once }, []) const handleSubmit = useCallback( async (payload: Record) => { if (!startedRef.current) return setSubmitting(true) setFlowError(null) try { const step = directMode ? await respondDirectFlow( executorBaseRef.current ?? "/auth/api/v3/flows/executor", slug, payload, flowQueryRef.current ) : await respondAuthFlow(slug, payload, flowQueryRef.current ?? flowQuery) setChallenge(step.challenge) setDone(step.done) setDenied(step.denied) if (step.done && !step.denied) { finishAuthentication(step.challenge) } } catch (err) { setFlowError(err instanceof Error ? err.message : "Échec de l'étape") } finally { setSubmitting(false) } }, [directMode, finishAuthentication, flowQuery, slug] ) const validationErrors = useMemo(() => flowValidationErrors(challenge), [challenge]) const formError = flowError ?? redirectError ?? validationErrors._form ?? null const title = useMemo(() => { if ((done && !denied) || isRecoveryEmailSent(slug, challenge)) return successTitle if (denied) return "Accès refusé" return flowTitle(challenge) || defaultTitle }, [challenge, defaultTitle, denied, done, slug, successTitle]) const description = useMemo(() => { if ((done && !denied) || isRecoveryEmailSent(slug, challenge)) return successDescription if (denied) { return "Ce parcours a été refusé. Vérifiez vos informations ou contactez le support." } return defaultDescription }, [challenge, defaultDescription, denied, done, slug, successDescription]) const component = flowComponent(challenge) const redirectUrl = flowRedirectUrl(challenge) const recoveryEmailSent = isRecoveryEmailSent(slug, challenge) const showSuccess = (done && !denied && !bridgeAuthentication) || recoveryEmailSent return ( {loading || bridging ? (
{bridging ? "Finalisation…" : "Chargement…"}
) : showSuccess ? (
{redirectUrl ? (

Redirection en cours…

) : null} {successExternal ? ( {successActionLabel} ) : ( {successActionLabel} )}
) : ( )}
) } export { AUTH_FLOW_SLUGS }