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:
137
legacy/allauth/components/views/MFAChooserView.tsx
Normal file
137
legacy/allauth/components/views/MFAChooserView.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { AuthenticatorType } from '../../defines'
|
||||
import { useAllauthAPI } from '../../contexts/APIContext'
|
||||
import { useStyles } from '../../contexts/StylesContext'
|
||||
import { AuthCard } from '../AuthCard'
|
||||
import { MFATOTPView } from './MFATOTPView'
|
||||
import { MFAWebAuthnView } from './MFAWebAuthnView'
|
||||
import { MFARecoveryCodesView } from './MFARecoveryCodesView'
|
||||
|
||||
const MFA_OPTIONS: Record<string, { label: string; description: string }> = {
|
||||
[AuthenticatorType.WEBAUTHN]: {
|
||||
label: 'Security Key / Passkey',
|
||||
description: 'Use your registered security key or passkey',
|
||||
},
|
||||
[AuthenticatorType.TOTP]: {
|
||||
label: 'Authenticator App',
|
||||
description: 'Enter a code from your authenticator app',
|
||||
},
|
||||
[AuthenticatorType.RECOVERY_CODES]: {
|
||||
label: 'Recovery Code',
|
||||
description: 'Use one of your recovery codes',
|
||||
},
|
||||
}
|
||||
|
||||
interface MFAChooserViewProps {
|
||||
types: string[]
|
||||
onSuccess?: () => void
|
||||
onCancel?: () => void
|
||||
isReauth?: boolean
|
||||
}
|
||||
|
||||
export function MFAChooserView({ types, onSuccess, onCancel, isReauth }: MFAChooserViewProps) {
|
||||
const api = useAllauthAPI()
|
||||
const styles = useStyles()
|
||||
const [selectedType, setSelectedType] = useState<string | null>(null)
|
||||
const [cancelling, setCancelling] = useState(false)
|
||||
|
||||
// Filter to only show options that are available
|
||||
const availableOptions = types
|
||||
.filter(type => MFA_OPTIONS[type])
|
||||
.map(type => ({ type, ...MFA_OPTIONS[type] }))
|
||||
|
||||
const handleCancel = async () => {
|
||||
setCancelling(true)
|
||||
try {
|
||||
await api.session.logout()
|
||||
onCancel?.()
|
||||
} catch {
|
||||
setCancelling(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = types.length > 1 ? () => setSelectedType(null) : undefined
|
||||
|
||||
// If a type is selected, show that method's view
|
||||
if (selectedType === AuthenticatorType.TOTP) {
|
||||
return (
|
||||
<MFATOTPView
|
||||
onSuccess={onSuccess}
|
||||
onCancel={onCancel}
|
||||
onBack={handleBack}
|
||||
isReauth={isReauth}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedType === AuthenticatorType.WEBAUTHN) {
|
||||
return (
|
||||
<MFAWebAuthnView
|
||||
onSuccess={onSuccess}
|
||||
onCancel={onCancel}
|
||||
onBack={handleBack}
|
||||
isReauth={isReauth}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedType === AuthenticatorType.RECOVERY_CODES) {
|
||||
return (
|
||||
<MFARecoveryCodesView
|
||||
onSuccess={onSuccess}
|
||||
onCancel={onCancel}
|
||||
onBack={handleBack}
|
||||
isReauth={isReauth}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Show chooser
|
||||
if (availableOptions.length === 0) {
|
||||
return (
|
||||
<AuthCard
|
||||
title="Two-Factor Authentication"
|
||||
subtitle="No authentication methods available."
|
||||
footerLinks={onCancel ? [
|
||||
{ label: 'Cancel and go back', onClick: handleCancel },
|
||||
] : []}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.card}>
|
||||
<h1 className={styles.title}>Two-Factor Authentication</h1>
|
||||
<p className={styles.subtitle}>Choose how you want to verify your identity.</p>
|
||||
|
||||
<div className={styles.form}>
|
||||
{availableOptions.map(option => (
|
||||
<button
|
||||
key={option.type}
|
||||
onClick={() => setSelectedType(option.type)}
|
||||
className={styles.providerButton}
|
||||
>
|
||||
<div style={{ textAlign: 'left' }}>
|
||||
<div style={{ fontWeight: 600 }}>{option.label}</div>
|
||||
<div style={{ fontSize: '0.8125rem', opacity: 0.7, marginTop: '0.25rem' }}>
|
||||
{option.description}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{onCancel && (
|
||||
<div className={styles.footer}>
|
||||
<button onClick={handleCancel} disabled={cancelling} className={styles.link}>
|
||||
{cancelling ? 'Cancelling...' : 'Cancel and go back'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user