Flatten to three packages + extract mizan-runtime
packages/
mizan-runtime/ Framework-agnostic state engine (~150 lines)
Context registry, batched invalidation, fetch primitives
mizan-django/ Django server adapter (was packages/mizan-rpc/adapters/django/)
Codegen moved to mizan-django/generate/
mizan-react/ React adapter (was packages/mizan-csr/adapters/react/)
Removed premature abstractions: mizan-ast, mizan-schema, mizan-rpc,
mizan-csr, mizan-ssr stub packages. The actual architecture is three
concrete packages, not five abstract layers.
mizan-runtime implements the v1 spec: registerContext with params,
scoped invalidation via microtask batching, server-driven invalidation
from mutation responses, mizanFetch for context bundles, mizanCall for
mutations.
264 Django + 33 React tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
142
packages/mizan-react/src/client/AuthContext.tsx
Normal file
142
packages/mizan-react/src/client/AuthContext.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useCallback,
|
||||
useMemo,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import { createDjangoCSRClient, Auth } from './index'
|
||||
import type { BaseUser, AuthDetails, AuthRoutes } from './types'
|
||||
|
||||
/**
|
||||
* Auth state provided by AuthContext.
|
||||
*/
|
||||
export interface AuthState<TUser extends BaseUser = BaseUser> {
|
||||
/** Current user (null if not authenticated) */
|
||||
user: TUser | null
|
||||
/** Whether auth state is loading */
|
||||
isLoading: boolean
|
||||
/** Refresh user from server */
|
||||
refresh: () => Promise<TUser | null>
|
||||
}
|
||||
|
||||
const Context = createContext<AuthState | null>(null)
|
||||
|
||||
/**
|
||||
* Default routes configuration.
|
||||
*/
|
||||
export const defaultRoutes: AuthRoutes = {
|
||||
login: '/auth/login',
|
||||
authenticated: '/dashboard',
|
||||
}
|
||||
|
||||
const RoutesContext = createContext<AuthRoutes>(defaultRoutes)
|
||||
|
||||
export interface AuthContextProps<TUser extends BaseUser = BaseUser> {
|
||||
children: ReactNode
|
||||
/** Initial user from SSR hydration (null if not authenticated) */
|
||||
user?: TUser | null
|
||||
/** API endpoint to fetch user data (default: '/api/auth/me/') */
|
||||
userEndpoint?: string
|
||||
/** Route configuration for guards */
|
||||
routes?: Partial<AuthRoutes>
|
||||
}
|
||||
|
||||
/**
|
||||
* Base auth context for Django-React apps.
|
||||
*
|
||||
* Provides user state from a simple /me endpoint.
|
||||
* For allauth integration, use AllauthContext instead.
|
||||
*/
|
||||
// Create client once at module level (session auth, no dynamic config needed)
|
||||
const client = createDjangoCSRClient(Auth.SESSION)
|
||||
|
||||
export function AuthContext<TUser extends BaseUser = BaseUser>({
|
||||
children,
|
||||
user: initialUser = null,
|
||||
userEndpoint = '/api/auth/me/',
|
||||
routes,
|
||||
}: AuthContextProps<TUser>) {
|
||||
const [user, setUser] = useState<TUser | null>(initialUser)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const refresh = useCallback(async (): Promise<TUser | null> => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const resp = await client.request('GET', userEndpoint)
|
||||
if (resp.ok) {
|
||||
const userData = await resp.json()
|
||||
setUser(userData)
|
||||
return userData
|
||||
} else if (resp.status === 401 || resp.status === 403) {
|
||||
setUser(null)
|
||||
return null
|
||||
}
|
||||
throw new Error(`Failed to fetch user: ${resp.status}`)
|
||||
} catch (e) {
|
||||
console.error('[AuthContext] Failed to fetch user:', e)
|
||||
return null
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [userEndpoint])
|
||||
|
||||
const authState = useMemo<AuthState<TUser>>(() => ({
|
||||
user,
|
||||
isLoading,
|
||||
refresh,
|
||||
}), [user, isLoading, refresh])
|
||||
|
||||
const routesValue = useMemo(() => ({
|
||||
...defaultRoutes,
|
||||
...routes,
|
||||
}), [routes])
|
||||
|
||||
return (
|
||||
<RoutesContext value={routesValue}>
|
||||
<Context value={authState}>
|
||||
{children}
|
||||
</Context>
|
||||
</RoutesContext>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access auth state.
|
||||
* Throws if used outside AuthContext.
|
||||
*/
|
||||
export function useAuthState<TUser extends BaseUser = BaseUser>(): AuthState<TUser> {
|
||||
const ctx = useContext(Context)
|
||||
if (!ctx) throw new Error('useAuthState must be used within AuthContext')
|
||||
return ctx as AuthState<TUser>
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access current user.
|
||||
* Returns null if not authenticated.
|
||||
*/
|
||||
export function useUser<TUser extends BaseUser = BaseUser>(): TUser | null {
|
||||
return useAuthState<TUser>().user
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access auth details (isAuthenticated, isStaff, etc.)
|
||||
*/
|
||||
export function useAuth(): AuthDetails {
|
||||
const user = useUser()
|
||||
return {
|
||||
isAuthenticated: user !== null,
|
||||
isStaff: user?.is_staff ?? false,
|
||||
isSuperuser: user?.is_superuser ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access route configuration.
|
||||
*/
|
||||
export function useAuthRoutes(): AuthRoutes {
|
||||
return useContext(RoutesContext)
|
||||
}
|
||||
Reference in New Issue
Block a user