Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced PasswordFieldBlock component for improved password input handling, including visibility toggle and strength evaluation. - Added SignupCredentialsFields component to streamline user signup with email availability checks and password confirmation. - Updated FlowChallengeForm to integrate new components, enhancing user experience during authentication. - Refactored CSS styles for consistent design across authentication components, ensuring a cohesive look and feel.
159 lines
4.7 KiB
TypeScript
159 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}
|
|
flowSlug={slug}
|
|
onSubmit={handleSubmit}
|
|
/>
|
|
)}
|
|
</AuthCard>
|
|
)
|
|
}
|
|
|
|
export { AUTH_FLOW_SLUGS }
|