Move allauth + auth UI to legacy/
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>
This commit is contained in:
@@ -1,142 +0,0 @@
|
||||
'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)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, type ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* Framework-agnostic router adapter.
|
||||
* Implement this interface for your framework (Next.js, Remix, etc.)
|
||||
*/
|
||||
export interface RouterAdapter {
|
||||
/** Navigate to a path (adds to history) */
|
||||
push: (path: string) => void
|
||||
/** Replace current path (no history entry) */
|
||||
replace: (path: string) => void
|
||||
/** Current pathname (e.g., "/account/login") */
|
||||
pathname: string
|
||||
/** Current search params */
|
||||
searchParams: URLSearchParams
|
||||
/** Get a specific route param (e.g., from /auth/[...path]) - optional */
|
||||
getParam?: (name: string) => string | string[] | undefined
|
||||
}
|
||||
|
||||
const Context = createContext<RouterAdapter | null>(null)
|
||||
|
||||
interface RouterContextProps {
|
||||
children: ReactNode
|
||||
router: RouterAdapter
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides router adapter to route guards.
|
||||
*/
|
||||
export function RouterContext({ children, router }: RouterContextProps) {
|
||||
return <Context value={router}>{children}</Context>
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access router adapter.
|
||||
*/
|
||||
export function useRouter(): RouterAdapter {
|
||||
const ctx = useContext(Context)
|
||||
if (!ctx) throw new Error('useRouter must be used within RouterContext')
|
||||
return ctx
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Next.js adapter for mizan/jwt.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* // In layout.tsx
|
||||
* import { NextAuthContext } from 'mizan/jwt/nextjs'
|
||||
*
|
||||
* export default function RootLayout({ children }) {
|
||||
* return (
|
||||
* <NextAuthContext user={user}>
|
||||
* {children}
|
||||
* </NextAuthContext>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react'
|
||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
|
||||
import type { RouterAdapter } from './RouterContext'
|
||||
import { RouterContext } from './RouterContext'
|
||||
import { AuthContext, type AuthContextProps } from './AuthContext'
|
||||
import type { BaseUser, AuthRoutes } from './types'
|
||||
|
||||
/**
|
||||
* Create a RouterAdapter from Next.js App Router hooks.
|
||||
*/
|
||||
export function useNextRouter(): RouterAdapter {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
return {
|
||||
push: (path: string) => router.push(path),
|
||||
replace: (path: string) => router.replace(path),
|
||||
pathname,
|
||||
searchParams: new URLSearchParams(searchParams.toString()),
|
||||
}
|
||||
}
|
||||
|
||||
export interface NextAuthContextProps<TUser extends BaseUser = BaseUser> {
|
||||
children: ReactNode
|
||||
/** Initial user from SSR hydration */
|
||||
user?: TUser | null
|
||||
/** API endpoint to fetch user data (default: '/api/auth/me/') */
|
||||
userEndpoint?: string
|
||||
/** Route configuration for guards */
|
||||
routes?: Partial<AuthRoutes>
|
||||
}
|
||||
|
||||
/**
|
||||
* Next.js-specific AuthContext that handles the router automatically.
|
||||
*/
|
||||
export function NextAuthContext<TUser extends BaseUser = BaseUser>({
|
||||
children,
|
||||
user,
|
||||
userEndpoint,
|
||||
routes,
|
||||
}: NextAuthContextProps<TUser>) {
|
||||
const router = useNextRouter()
|
||||
|
||||
return (
|
||||
<RouterContext router={router}>
|
||||
<AuthContext user={user} userEndpoint={userEndpoint} routes={routes}>
|
||||
{children}
|
||||
</AuthContext>
|
||||
</RouterContext>
|
||||
)
|
||||
}
|
||||
@@ -11,12 +11,7 @@ import {
|
||||
|
||||
// Re-export everything from main entry for convenience
|
||||
export * from './index'
|
||||
|
||||
// Re-export auth components for React users
|
||||
export * from './AuthContext'
|
||||
export * from '../jwt/JWTContext'
|
||||
export * from './RouterContext'
|
||||
export * from './routing'
|
||||
export type * from './types'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, type ReactNode } from 'react'
|
||||
import { useRouter } from './RouterContext'
|
||||
import { useAuth, useAuthRoutes } from './AuthContext'
|
||||
|
||||
/**
|
||||
* Route guard that only renders children if the user is authenticated.
|
||||
* Redirects to login page if not authenticated.
|
||||
*/
|
||||
export function UserRoute({ children }: { children: ReactNode }) {
|
||||
const router = useRouter()
|
||||
const routes = useAuthRoutes()
|
||||
const { isAuthenticated } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
const searchParams = router.searchParams.toString()
|
||||
const currentPath = searchParams
|
||||
? `${router.pathname}?${searchParams}`
|
||||
: router.pathname
|
||||
const next = encodeURIComponent(currentPath)
|
||||
router.replace(`${routes.login}?next=${next}`)
|
||||
}
|
||||
}, [isAuthenticated, router, routes.login])
|
||||
|
||||
if (!isAuthenticated) return null
|
||||
return children
|
||||
}
|
||||
|
||||
/**
|
||||
* Route guard that only renders children if the user is authenticated AND is staff.
|
||||
* Redirects to login if not authenticated, or to authenticated route if not staff.
|
||||
*/
|
||||
export function StaffRoute({ children }: { children: ReactNode }) {
|
||||
const router = useRouter()
|
||||
const routes = useAuthRoutes()
|
||||
const { isAuthenticated, isStaff } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
const searchParams = router.searchParams.toString()
|
||||
const currentPath = searchParams
|
||||
? `${router.pathname}?${searchParams}`
|
||||
: router.pathname
|
||||
const next = encodeURIComponent(currentPath)
|
||||
router.replace(`${routes.login}?next=${next}`)
|
||||
} else if (!isStaff) {
|
||||
router.replace(routes.authenticated)
|
||||
}
|
||||
}, [isAuthenticated, isStaff, router, routes])
|
||||
|
||||
if (!isAuthenticated || !isStaff) return null
|
||||
return children
|
||||
}
|
||||
|
||||
/**
|
||||
* Route guard that only renders children if the user is NOT authenticated.
|
||||
* Redirects to authenticated route if already logged in.
|
||||
*/
|
||||
export function AnonymousRoute({ children }: { children: ReactNode }) {
|
||||
const router = useRouter()
|
||||
const routes = useAuthRoutes()
|
||||
const { isAuthenticated } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
router.replace(routes.authenticated)
|
||||
}
|
||||
}, [isAuthenticated, routes.authenticated, router])
|
||||
|
||||
if (isAuthenticated) return null
|
||||
return children
|
||||
}
|
||||
Reference in New Issue
Block a user