Files
mizan/legacy/allauth/components/settings/MFASection.tsx
Ryth Azhur 27c30d7e50 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>
2026-04-07 03:41:22 -04:00

172 lines
5.8 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { useAllauthAPI } from '../../contexts/APIContext'
import { useStyles } from '../../contexts/StylesContext'
import { SettingsSection, SettingsItem, Badge, Button } from './SettingsComponents'
import type { Authenticator, TOTPStatus } from '../../types'
interface TOTPSetup {
secret: string
totp_url: string
}
export function MFASection() {
const api = useAllauthAPI()
const [authenticators, setAuthenticators] = useState<Authenticator[]>([])
const [loading, setLoading] = useState(true)
const [available, setAvailable] = useState(true)
const fetchAuthenticators = async () => {
try {
const res = await api.mfa.list()
if (res.status === 200 && res.data) {
setAuthenticators(res.data as Authenticator[])
} else {
// Non-200 status means MFA not available
setAvailable(false)
}
} catch {
setAvailable(false)
}
setLoading(false)
}
useEffect(() => { fetchAuthenticators() }, [])
if (loading || !available) return null
const hasTOTP = authenticators.some(a => a.type === 'totp')
return (
<SettingsSection title="Two-Factor Authentication">
<TOTPSubsection
hasTOTP={hasTOTP}
onUpdate={fetchAuthenticators}
/>
{hasTOTP && (
<RecoveryCodesSubsection />
)}
</SettingsSection>
)
}
// --- TOTP Subsection ---
function TOTPSubsection({ hasTOTP, onUpdate }: { hasTOTP: boolean; onUpdate: () => void }) {
const api = useAllauthAPI()
const styles = useStyles()
const [showSetup, setShowSetup] = useState(false)
const [setup, setSetup] = useState<TOTPSetup | null>(null)
const [code, setCode] = useState('')
const handleStartSetup = async () => {
const res = await api.mfa.totp.getStatus()
// allauth returns TOTP status with secret and totp_url for setup
const data = res.data as TOTPStatus | undefined
if (data?.secret && data?.totp_url) {
setSetup({ secret: data.secret, totp_url: data.totp_url })
setShowSetup(true)
}
}
const handleActivate = async (e: React.FormEvent) => {
e.preventDefault()
const res = await api.mfa.totp.activate(code)
if (res.status === 200) {
setShowSetup(false)
setSetup(null)
setCode('')
onUpdate()
}
}
const handleDeactivate = async () => {
if (!confirm('Disable authenticator app?')) return
await api.mfa.totp.deactivate()
onUpdate()
}
return (
<>
<h3 className={styles.settingsSubtitle}>Authenticator App</h3>
{showSetup && setup ? (
<div className={styles.totpSetup}>
<p>Scan this QR code with your authenticator app:</p>
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(setup.totp_url)}`}
alt="TOTP QR Code"
className={styles.qrCode}
/>
<p className={styles.settingsItemMeta}>Secret: {setup.secret}</p>
<form onSubmit={handleActivate} className={styles.inlineForm}>
<div className={styles.field}>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Verification Code"
className={styles.fieldInput}
/>
</div>
<Button type="submit">Activate</Button>
<Button type="button" variant="secondary" onClick={() => setShowSetup(false)}>
Cancel
</Button>
</form>
</div>
) : hasTOTP ? (
<SettingsItem
label={<>Authenticator App <Badge variant="success">Active</Badge></>}
actions={<Button variant="danger" onClick={handleDeactivate}>Disable</Button>}
/>
) : (
<Button onClick={handleStartSetup}>Set Up Authenticator</Button>
)}
</>
)
}
// --- Recovery Codes Subsection ---
function RecoveryCodesSubsection() {
const api = useAllauthAPI()
const styles = useStyles()
const [codes, setCodes] = useState<string[]>([])
const handleView = async () => {
const res = await api.mfa.recoveryCodes.list()
if (res.status === 200) {
setCodes(res.data?.unused_codes || [])
}
}
const handleRegenerate = async () => {
if (!confirm('Generate new codes? Old codes will stop working.')) return
const res = await api.mfa.recoveryCodes.regenerate()
if (res.status === 200) {
setCodes(res.data?.unused_codes || [])
}
}
return (
<>
<h3 className={styles.settingsSubtitle}>Recovery Codes</h3>
{codes.length > 0 ? (
<div>
<div className={styles.recoveryCodes}>
{codes.map((code, i) => <span key={i}>{code}</span>)}
</div>
<p className={styles.settingsItemMeta}>Store these safely. Each code works once.</p>
<Button variant="secondary" onClick={handleRegenerate}>Regenerate</Button>
</div>
) : (
<Button variant="secondary" onClick={handleView}>View Recovery Codes</Button>
)}
</>
)
}