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>
154 lines
4.7 KiB
TypeScript
154 lines
4.7 KiB
TypeScript
'use client'
|
|
|
|
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
|
import { useDjangoCSRClient, Auth } from 'mizan/client/react'
|
|
import { useMizan, useMizanContext } from 'mizan'
|
|
import { getAuthDetails, createAPI } from '../api'
|
|
import type { AllauthResponse } from '../types'
|
|
import getAuthChangeEvent from '../events'
|
|
|
|
export interface AuthState {
|
|
config: AllauthResponse
|
|
auth: AllauthResponse
|
|
event: string
|
|
refresh: (newAuth?: AllauthResponse) => Promise<AllauthResponse>
|
|
}
|
|
|
|
const Context = createContext<AuthState | null>(null)
|
|
|
|
export interface AuthContextProps {
|
|
children: ReactNode
|
|
/** Initial config from hydration */
|
|
config: AllauthResponse
|
|
/** Initial auth from hydration */
|
|
auth: AllauthResponse
|
|
}
|
|
|
|
export function AuthContext({
|
|
children,
|
|
config,
|
|
auth: initialAuth,
|
|
}: AuthContextProps) {
|
|
const client = useDjangoCSRClient(Auth.SESSION)
|
|
const { refreshAllContexts } = useMizan()
|
|
const [auth, setAuth] = useState(initialAuth)
|
|
const [event, setEvent] = useState('')
|
|
const prevAuth = useRef(initialAuth)
|
|
|
|
// Create API for refresh operations
|
|
const baseAPI = useMemo(() => {
|
|
const authRequest = async (method: string, path: string, data?: any, headers?: Record<string, string>) => {
|
|
const resp = await client.request(method, `/_allauth/browser/v1${path}`, data, headers)
|
|
if (resp.status >= 500) {
|
|
throw new Error(`Allauth request failed: ${resp.status} ${resp.statusText}`)
|
|
}
|
|
return resp.json()
|
|
}
|
|
return createAPI((method, path, data?, headers?) =>
|
|
authRequest(method, path, { ...(data as object), client: 'browser' }, headers)
|
|
)
|
|
}, [client])
|
|
|
|
const refresh = useCallback(async (newAuth?: AllauthResponse): Promise<AllauthResponse> => {
|
|
const authState = newAuth ?? await baseAPI.session.getStatus()
|
|
setAuth(authState)
|
|
|
|
// Refresh all Django contexts (user data, permissions, etc.)
|
|
await refreshAllContexts()
|
|
|
|
return authState
|
|
}, [baseAPI, refreshAllContexts])
|
|
|
|
useEffect(() => {
|
|
if (prevAuth.current && auth) {
|
|
setEvent(getAuthChangeEvent(prevAuth.current, auth))
|
|
}
|
|
prevAuth.current = auth
|
|
}, [auth])
|
|
|
|
const contextValue = useMemo(() => ({
|
|
config, auth, event, refresh
|
|
}), [config, auth, event, refresh])
|
|
|
|
return (
|
|
<Context value={contextValue}>
|
|
{children}
|
|
</Context>
|
|
)
|
|
}
|
|
|
|
export function useAuthContext(): AuthState {
|
|
const ctx = useContext(Context)
|
|
if (!ctx) throw new Error('useAuthContext must be used within AuthContext')
|
|
return ctx
|
|
}
|
|
|
|
export function useAuth() {
|
|
return getAuthDetails(useAuthContext().auth)
|
|
}
|
|
|
|
/**
|
|
* Base user interface expected by Allauth.
|
|
* Products can extend this with additional fields.
|
|
*/
|
|
export interface AllauthUser {
|
|
email?: string
|
|
first_name?: string
|
|
last_name?: string
|
|
is_staff?: boolean
|
|
is_superuser?: boolean
|
|
}
|
|
|
|
/**
|
|
* Get the current user from MizanProvider.
|
|
*
|
|
* This uses the generic mizan hook to access the 'user' context.
|
|
* The backend defines this context in lib/mizan/allauth/contexts.py:
|
|
*
|
|
* @client(context='global')
|
|
* def user(request) -> UserOutput | None:
|
|
* ...
|
|
*
|
|
* @typeParam T - User type (defaults to AllauthUser, products can use more specific types)
|
|
*/
|
|
export function useUser<T extends AllauthUser = AllauthUser>(): T {
|
|
const user = useMizanContext<T>('user')
|
|
// Return empty object cast to T if user is undefined (not loaded)
|
|
// This matches the previous behavior and allows optional chaining
|
|
return (user ?? {}) as T
|
|
}
|
|
|
|
export function useConfig() {
|
|
return useAuthContext().config
|
|
}
|
|
|
|
/**
|
|
* Hook to access backend feature flags from the allauth configuration.
|
|
*/
|
|
export function useFeatures() {
|
|
const config = useConfig()
|
|
const data = config?.data as {
|
|
account?: {
|
|
is_open_for_signup?: boolean
|
|
login_by_code_enabled?: boolean
|
|
email_verification_by_code_enabled?: boolean
|
|
}
|
|
mfa?: {
|
|
supported_types?: string[]
|
|
}
|
|
socialaccount?: {
|
|
providers?: any[]
|
|
}
|
|
} | undefined
|
|
|
|
return {
|
|
signupEnabled: data?.account?.is_open_for_signup ?? true,
|
|
loginByCodeEnabled: data?.account?.login_by_code_enabled ?? false,
|
|
emailVerificationByCodeEnabled: data?.account?.email_verification_by_code_enabled ?? false,
|
|
mfaEnabled: (data?.mfa?.supported_types?.length ?? 0) > 0,
|
|
mfaTypes: data?.mfa?.supported_types ?? [],
|
|
socialLoginEnabled: (data?.socialaccount?.providers?.length ?? 0) > 0,
|
|
socialProviders: data?.socialaccount?.providers ?? [],
|
|
}
|
|
}
|