allauth/ (44 files) is a django-allauth React UI — a separate concern from the Mizan protocol. Moved to legacy/ pending extraction into a standalone mizan-django-allauth package. Also moved to legacy/: - client/AuthContext.tsx — generic auth state from /me endpoint - client/RouterContext.tsx — framework-agnostic router adapter - client/routing.tsx — UserRoute/StaffRoute/AnonymousRoute guards - client/nextjs.tsx — Next.js router adapter for auth These are auth UI infrastructure, not Mizan protocol. The Mizan core only needs JWT for auth header selection (jwt/ stays — MizanProvider depends on useJWT() to decide between Bearer and session auth). Cleaned up re-exports in client/react.ts and vitest aliases. 33 React tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
221 lines
6.7 KiB
TypeScript
221 lines
6.7 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { useRouter } from '../contexts/RouterContext'
|
|
import { useAllauthAPI } from '../contexts/APIContext'
|
|
import { useAllauthConfig } from '../contexts/ConfigContext'
|
|
import { DjangoFlowPaths } from '../config'
|
|
import { AuthCard } from './AuthCard'
|
|
import { AuthDjangoForm } from './AuthDjangoForm'
|
|
|
|
interface AllauthRouterProps {
|
|
/** Called after successful completion of any flow */
|
|
onComplete?: () => void
|
|
/** Called when user wants to go back to login */
|
|
onLoginClick?: () => void
|
|
}
|
|
|
|
/**
|
|
* AllauthRouter handles Django-initiated flows (email verification, password reset, OAuth).
|
|
*
|
|
* Mount this at a catch-all route matching your basePath config:
|
|
* app/auth/[...path]/page.tsx -> <AllauthRouter />
|
|
*
|
|
* The path determines which flow to render:
|
|
* /auth/verify-email/[key] -> Email verification
|
|
* /auth/reset-password?key=xxx -> Password reset form
|
|
* /auth/oauth/callback -> OAuth completion
|
|
*/
|
|
export function AllauthRouter({ onComplete, onLoginClick }: AllauthRouterProps) {
|
|
const router = useRouter()
|
|
const config = useAllauthConfig()
|
|
|
|
// Parse the path segments after basePath
|
|
// The router provides getParam('path') which returns the catch-all segments
|
|
const pathParam = router.getParam('path')
|
|
const pathSegments = Array.isArray(pathParam) ? pathParam : pathParam ? [pathParam] : []
|
|
const path = pathSegments.length > 0 ? `/${pathSegments.join('/')}` : '/'
|
|
|
|
// Determine which flow based on path
|
|
if (path.startsWith(DjangoFlowPaths.VERIFY_EMAIL)) {
|
|
const key = pathSegments[1] || router.searchParams.get('key')
|
|
return (
|
|
<EmailVerifyView
|
|
verificationKey={key}
|
|
onComplete={onComplete}
|
|
onLoginClick={onLoginClick}
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (path.startsWith(DjangoFlowPaths.RESET_PASSWORD)) {
|
|
const key = pathSegments[1] || router.searchParams.get('key')
|
|
return (
|
|
<PasswordResetView
|
|
resetKey={key}
|
|
onComplete={onComplete}
|
|
onLoginClick={onLoginClick}
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (path.startsWith(DjangoFlowPaths.OAUTH_ERROR)) {
|
|
return (
|
|
<OAuthErrorView
|
|
onLoginClick={onLoginClick}
|
|
/>
|
|
)
|
|
}
|
|
|
|
// Unknown path
|
|
return (
|
|
<AuthCard
|
|
title="Not Found"
|
|
subtitle="This page doesn't exist."
|
|
footerLinks={onLoginClick ? [
|
|
{ label: 'Back to Sign In', onClick: onLoginClick },
|
|
] : []}
|
|
/>
|
|
)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Email Verification View
|
|
// ----------------------------------------------------------------------------
|
|
|
|
interface EmailVerifyViewProps {
|
|
verificationKey: string | null | undefined
|
|
onComplete?: () => void
|
|
onLoginClick?: () => void
|
|
}
|
|
|
|
function EmailVerifyView({ verificationKey, onComplete, onLoginClick }: EmailVerifyViewProps) {
|
|
const api = useAllauthAPI()
|
|
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading')
|
|
const [error, setError] = useState('')
|
|
|
|
useEffect(() => {
|
|
if (!verificationKey) {
|
|
setStatus('error')
|
|
setError('Invalid verification link')
|
|
return
|
|
}
|
|
|
|
const verify = async () => {
|
|
const res = await api.account.emails.verification.confirmKey(verificationKey)
|
|
|
|
if (res.status === 200) {
|
|
setStatus('success')
|
|
if (onComplete) {
|
|
setTimeout(onComplete, 2000)
|
|
}
|
|
} else {
|
|
setStatus('error')
|
|
setError(res.errors?.[0]?.message || 'Invalid or expired verification link')
|
|
}
|
|
}
|
|
|
|
verify()
|
|
}, [verificationKey, api, onComplete])
|
|
|
|
if (status === 'loading') {
|
|
return <AuthCard title="" loading loadingText="Verifying your email..." />
|
|
}
|
|
|
|
if (status === 'success') {
|
|
return (
|
|
<AuthCard
|
|
title="Email Verified"
|
|
subtitle="Your email has been verified successfully."
|
|
/>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<AuthCard
|
|
title="Verification Failed"
|
|
error={error}
|
|
footerLinks={onLoginClick ? [
|
|
{ label: 'Back to Sign In', onClick: onLoginClick },
|
|
] : []}
|
|
/>
|
|
)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Password Reset View
|
|
// ----------------------------------------------------------------------------
|
|
|
|
interface PasswordResetViewProps {
|
|
resetKey: string | null | undefined
|
|
onComplete?: () => void
|
|
onLoginClick?: () => void
|
|
}
|
|
|
|
function PasswordResetView({ resetKey, onComplete, onLoginClick }: PasswordResetViewProps) {
|
|
const [success, setSuccess] = useState(false)
|
|
|
|
if (!resetKey) {
|
|
return (
|
|
<AuthCard
|
|
title="Invalid Link"
|
|
subtitle="This password reset link is invalid or has expired."
|
|
footerLinks={onLoginClick ? [
|
|
{ label: 'Back to Sign In', onClick: onLoginClick },
|
|
] : []}
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (success) {
|
|
return (
|
|
<AuthCard
|
|
title="Password Changed"
|
|
subtitle="Your password has been successfully reset."
|
|
footerLinks={onLoginClick ? [
|
|
{ label: 'Sign In', onClick: onLoginClick },
|
|
] : []}
|
|
/>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<AuthDjangoForm
|
|
formName="reset_password_from_key"
|
|
onSuccess={() => {
|
|
setSuccess(true)
|
|
// Give user time to see success message before redirect
|
|
if (onComplete) {
|
|
setTimeout(onComplete, 2000)
|
|
}
|
|
}}
|
|
footerLinks={onLoginClick ? [
|
|
{ href: '#', label: 'Back to Sign In', onClick: onLoginClick },
|
|
] : []}
|
|
/>
|
|
)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// OAuth Error View
|
|
// ----------------------------------------------------------------------------
|
|
|
|
interface OAuthErrorViewProps {
|
|
onLoginClick?: () => void
|
|
}
|
|
|
|
function OAuthErrorView({ onLoginClick }: OAuthErrorViewProps) {
|
|
const router = useRouter()
|
|
const error = router.searchParams.get('error') || 'An error occurred during authentication'
|
|
|
|
return (
|
|
<AuthCard
|
|
title="Authentication Failed"
|
|
error={error}
|
|
footerLinks={onLoginClick ? [
|
|
{ label: 'Back to Sign In', onClick: onLoginClick },
|
|
] : []}
|
|
/>
|
|
)
|
|
}
|