ultisuite-client/components/auth/auth-flow-page.tsx
R3D347HR4Y be9133e220
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat: introduce AuthConnectButton component for streamlined authentication actions
- Added AuthConnectButton component to centralize and simplify authentication button rendering across various forms.
- Updated existing authentication components to utilize AuthConnectButton, enhancing code reusability and maintainability.
- Refactored CSS styles for consistent button appearance and layout in authentication flows.
2026-06-19 22:54:04 +02:00

158 lines
4.7 KiB
TypeScript

"use client"
import { useCallback, useEffect, useMemo, 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,
flowComponent,
flowRedirectUrl,
flowTitle,
flowValidationErrors,
isRecoveryEmailSent,
respondAuthFlow,
startAuthFlow,
type AuthFlowSlug,
type FlowChallenge,
} from "@/lib/auth/flow-api"
type AuthFlowPageProps = {
slug: AuthFlowSlug
defaultTitle: string
defaultDescription: string
successTitle: string
successDescription: string
successActionLabel: string
successHref: string
/** Full document navigation required (OIDC login routes). */
successExternal?: boolean
footer: React.ReactNode
onSuccess?: () => void
}
export function AuthFlowPage({
slug,
defaultTitle,
defaultDescription,
successTitle,
successDescription,
successActionLabel,
successHref,
successExternal = false,
footer,
onSuccess,
}: AuthFlowPageProps) {
const [sessionId, setSessionId] = useState<string | null>(null)
const [challenge, setChallenge] = useState<FlowChallenge | null>(null)
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const [done, setDone] = useState(false)
const [denied, setDenied] = useState(false)
const bootstrap = useCallback(async () => {
setLoading(true)
setError(null)
try {
const step = await startAuthFlow(slug)
setSessionId(step.sessionId)
setChallenge(step.challenge)
setDone(step.done)
setDenied(step.denied)
if (step.done && !step.denied) {
onSuccess?.()
}
} catch (err) {
setError(err instanceof Error ? err.message : "Impossible de démarrer le parcours")
} finally {
setLoading(false)
}
}, [onSuccess, slug])
useEffect(() => {
void bootstrap()
}, [bootstrap])
const validationErrors = useMemo(() => flowValidationErrors(challenge), [challenge])
const formError = error ?? 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])
async function handleSubmit(payload: Record<string, unknown>) {
if (!sessionId) return
setSubmitting(true)
setError(null)
try {
const step = await respondAuthFlow(slug, payload)
setSessionId(step.sessionId)
setChallenge(step.challenge)
setDone(step.done)
setDenied(step.denied)
if (step.done && !step.denied) {
onSuccess?.()
}
} catch (err) {
setError(err instanceof Error ? err.message : "Échec de l'étape")
} finally {
setSubmitting(false)
}
}
const component = flowComponent(challenge)
const redirectUrl = flowRedirectUrl(challenge)
const recoveryEmailSent = isRecoveryEmailSent(slug, challenge)
const showSuccess = (done && !denied) || recoveryEmailSent
return (
<AuthCard title={title} description={description} error={formError} footer={footer}>
{loading ? (
<div className="flex justify-center py-8">
<Loader2 className="size-6 animate-spin text-muted-foreground" aria-hidden />
<span className="sr-only">Chargement</span>
</div>
) : showSuccess ? (
<div className="flex flex-col items-center gap-4">
{redirectUrl ? (
<p className="text-center text-sm text-muted-foreground">
Redirection en cours
</p>
) : null}
{successExternal ? (
<AuthConnectButton href={successHref}>
{successActionLabel}
</AuthConnectButton>
) : (
<AuthConnectButton as="link" href={successHref}>
{successActionLabel}
</AuthConnectButton>
)}
</div>
) : (
<FlowChallengeForm
challenge={challenge}
component={component}
submitting={submitting}
fieldErrors={validationErrors}
onSubmit={handleSubmit}
/>
)}
</AuthCard>
)
}
export { AUTH_FLOW_SLUGS }