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>
100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
'use client'
|
|
|
|
import { ReactNode, useState, useEffect } from 'react'
|
|
import { AuthDetails, AuthError, AuthResponse, getAuthDetails } from '../api'
|
|
import { useAuthContext } from '../contexts/AuthContext'
|
|
import { useStyles } from '../contexts/StylesContext'
|
|
|
|
interface AuthForm {
|
|
submit: () => void
|
|
authDetails: AuthDetails
|
|
fetching: boolean
|
|
response: AuthResponse | null
|
|
errors: AuthError[]
|
|
}
|
|
|
|
export default function useAuthForm(
|
|
submissionAction: () => Promise<AuthResponse>,
|
|
responseAction?: (response: AuthResponse, authDetails: AuthDetails) => void,
|
|
): AuthForm {
|
|
const auth = useAuthContext().auth
|
|
const [fetching, setFetching] = useState<boolean>(false)
|
|
const [response, setResponse] = useState<AuthResponse | null>(null)
|
|
const [errors, setErrors] = useState<AuthError[]>([])
|
|
const [authDetails, setAuthDetails] = useState<AuthDetails>(getAuthDetails(auth))
|
|
|
|
function submit() {
|
|
setFetching(true)
|
|
submissionAction()
|
|
.then((r) => {
|
|
setResponse(r)
|
|
setErrors(r.errors || [])
|
|
setFetching(false)
|
|
if (r && responseAction) {
|
|
responseAction(r, authDetails)
|
|
}
|
|
setAuthDetails(getAuthDetails(auth))
|
|
})
|
|
.catch((e) => {
|
|
console.error(e)
|
|
setFetching(false)
|
|
})
|
|
}
|
|
|
|
return { submit, authDetails, fetching, response, errors }
|
|
}
|
|
|
|
interface AuthFieldProps {
|
|
title: string
|
|
name: string
|
|
type: string
|
|
init: string
|
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
|
authErrors: AuthError[]
|
|
placeholder?: string
|
|
children?: ReactNode
|
|
}
|
|
|
|
export function AuthField({
|
|
title,
|
|
name,
|
|
type,
|
|
init,
|
|
onChange,
|
|
authErrors,
|
|
placeholder,
|
|
children,
|
|
}: AuthFieldProps) {
|
|
const styles = useStyles()
|
|
const [mounted, setMounted] = useState(false)
|
|
const fieldErrors = authErrors.filter(err => err.param === name)
|
|
|
|
useEffect(() => {
|
|
setMounted(true)
|
|
}, [])
|
|
|
|
return (
|
|
<div className={styles.field}>
|
|
<label className={styles.fieldLabel}>{title}</label>
|
|
{mounted ? (
|
|
<input
|
|
type={type}
|
|
value={init}
|
|
onChange={onChange}
|
|
placeholder={placeholder}
|
|
className={styles.fieldInput}
|
|
autoComplete="off"
|
|
/>
|
|
) : (
|
|
<div className={styles.fieldInput} style={{ minHeight: '2.75rem' }} />
|
|
)}
|
|
{fieldErrors.map((err, i) => (
|
|
<p key={i} className={styles.fieldError}>{err.message}</p>
|
|
))}
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
|