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>
114 lines
3.9 KiB
TypeScript
114 lines
3.9 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useAllauthAPI } from '../../contexts/APIContext'
|
|
import { useAuthContext } from '../../contexts/AuthContext'
|
|
import { useStyles } from '../../contexts/StylesContext'
|
|
|
|
interface MFAWebAuthnViewProps {
|
|
onSuccess?: () => void
|
|
onCancel?: () => void
|
|
onBack?: () => void
|
|
isReauth?: boolean
|
|
}
|
|
|
|
export function MFAWebAuthnView({ onSuccess, onCancel, onBack, isReauth }: MFAWebAuthnViewProps) {
|
|
const api = useAllauthAPI()
|
|
const { refresh } = useAuthContext()
|
|
const styles = useStyles()
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [authenticating, setAuthenticating] = useState(false)
|
|
const [cancelling, setCancelling] = useState(false)
|
|
|
|
const handleCancel = async () => {
|
|
setCancelling(true)
|
|
try {
|
|
await api.session.logout()
|
|
onCancel?.()
|
|
} catch {
|
|
setCancelling(false)
|
|
}
|
|
}
|
|
|
|
const handleWebAuthn = async () => {
|
|
setError(null)
|
|
setAuthenticating(true)
|
|
|
|
try {
|
|
const { startAuthentication } = await import('@simplewebauthn/browser')
|
|
|
|
// Get challenge from server
|
|
const optionsRes = isReauth
|
|
? await api.webauthn.requestOptions.reauthentication()
|
|
: await api.webauthn.requestOptions.authentication()
|
|
|
|
if (optionsRes.status !== 200 || !optionsRes.data?.request_options?.publicKey) {
|
|
throw new Error('Failed to get authentication options')
|
|
}
|
|
|
|
// Perform WebAuthn authentication
|
|
// The allauth API returns { request_options: { publicKey: {...} } }
|
|
// @simplewebauthn/browser v13+ expects { optionsJSON: ... }
|
|
const credential = await startAuthentication({ optionsJSON: optionsRes.data.request_options.publicKey as any })
|
|
|
|
// Verify with server
|
|
const res = isReauth
|
|
? await api.webauthn.reauthenticate(credential)
|
|
: await api.webauthn.authenticate(credential)
|
|
|
|
if (res.status === 200) {
|
|
await refresh()
|
|
onSuccess?.()
|
|
} else {
|
|
setError('Authentication failed. Please try again.')
|
|
}
|
|
} catch (e: any) {
|
|
if (e.name === 'AbortError' || e.name === 'NotAllowedError') {
|
|
setError(null)
|
|
} else {
|
|
setError(e.message || 'Failed to authenticate with security key')
|
|
}
|
|
} finally {
|
|
setAuthenticating(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
<div className={styles.card}>
|
|
<h1 className={styles.title}>Security Key</h1>
|
|
<p className={styles.subtitle}>Use your security key to verify your identity.</p>
|
|
|
|
{error && (
|
|
<div className={styles.error}>
|
|
<p>{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className={styles.form}>
|
|
<button
|
|
onClick={handleWebAuthn}
|
|
disabled={authenticating}
|
|
className={styles.submit}
|
|
>
|
|
{authenticating ? 'Waiting for security key...' : 'Use Security Key'}
|
|
</button>
|
|
</div>
|
|
|
|
<div className={styles.footer}>
|
|
{onBack && (
|
|
<button onClick={onBack} className={styles.link}>
|
|
Use a different method
|
|
</button>
|
|
)}
|
|
{onCancel && (
|
|
<button onClick={handleCancel} disabled={cancelling} className={styles.link}>
|
|
{cancelling ? 'Cancelling...' : 'Cancel'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|