Move allauth + auth UI to legacy/
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>
This commit is contained in:
220
legacy/allauth/components/AllauthRouter.tsx
Normal file
220
legacy/allauth/components/AllauthRouter.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
'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 },
|
||||
] : []}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user