'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 } const Context = createContext(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) => { 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 => { 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 ( {children} ) } 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 { const user = useMizanContext('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 ?? [], } }