9.4 KiB
Architecture
Analysis Date: 2026-05-07
Pattern Overview
Overall: Next.js 15 App Router with client-centric admin dashboard, role-based access control (RBAC) via localStorage permission matrix, and layered API abstraction.
Key Characteristics:
- Client-side rendering-first pattern with permission checks in layout/shell components
- Token-based authentication with Axios interceptors for transparent token injection
- Role-based module access controlled by permission matrix in
lib/permissions.ts - Form handling via React Hook Form + Zod for validation
- Shadcn-style UI components copied into
components/ui/(not npm packages)
Layers
Page Layer (Presentation/Routing):
- Purpose: App Router page components that mount client-side shells and load data
- Location:
app/[module]/page.tsx(e.g.,app/outfits/page.tsx,app/login/page.tsx) - Contains: "use client" page entries, form pages, list views
- Depends on: DashboardShell, custom feature components, API clients
- Used by: Next.js App Router, browser navigation
Component Layer (UI & Business Logic):
- Purpose: Reusable business components (modals, dialogs, tables, feature-specific UI)
- Location:
components/(feature folders) andcomponents/ui/(primitives) - Contains: DashboardShell, Sidebar, feature dialogs (AddOutfitDialog, DeleteConfirmationDialog), stat cards
- Depends on: UI primitives, API clients, React Hook Form, icons (lucide-react)
- Used by: Page components
API Client Layer (HTTP Communication):
- Purpose: Axios-based API communication with interceptors for auth, error handling
- Location:
lib/api/(e.g.,lib/api/client.ts,lib/api/auth.ts,lib/api/outfits.ts) - Contains: API singleton instance, interceptors, module-specific request/response adapters
- Depends on: Axios, localStorage (for token retrieval), environment variables
- Used by: Page components and feature components
Permission/Auth Layer (Access Control):
- Purpose: Role-based module visibility and path-level permission checks
- Location:
lib/permissions.ts(RBAC matrix),middleware.ts(route protection) - Contains: PERMISSION_MATRIX (role → module[]), utility functions (hasPermission, getUserRole, getModuleFromPath)
- Depends on: localStorage.user_role, localStorage.is_superuser
- Used by: DashboardShell, Sidebar, middleware
Utility Layer:
- Purpose: Type definitions, helpers, environment detection
- Location:
lib/utils.ts,lib/api/types.ts,lib/api/adapters.ts - Contains: Tailwind utilities (cn()), type mappings, mock data
- Depends on: Nothing (standalone)
- Used by: All layers
Data Flow
Authentication Flow (Login → Authenticated State):
- User fills login form on
app/login/page.tsx - Form submission calls
emailLogin(email, password)fromlib/api/auth.ts - Axios POSTs to
{NEXT_PUBLIC_API_BASE_URL}/v1/admin/login/ - Response contains
token,role,is_superuser saveAuthToken(token, isSuperUser, role)stores to localStorage and Cookie- Page redirects to
/(dashboard) - Middleware checks Cookie
auth_token; on subsequent page loads, Axios request interceptor injects token from localStorage
Protected Page Access Flow:
- User navigates to protected route (e.g.,
/outfits) - Middleware checks cookie; if missing, redirects to
/login - Page mounts, DashboardShell component renders
- DashboardShell calls
hasPathPermission(pathname)at mount - Function checks localStorage.user_role against PERMISSION_MATRIX
- If denied, renders access-denied UI; if allowed, renders children
- Sidebar calls
hasPermission(module)for each menu item (mounted only) to filter visible items - Sidebar also displays current role from localStorage.user_role
Data Fetching Flow (Page → Component → API → Backend):
- Page component (e.g.,
app/outfits/page.tsx) mounts as "use client" - Page renders feature components (e.g., OutfitsList, OutfitsTable)
- Feature component calls
getOutfits(params)fromlib/api/outfits.ts - API function calls
apiClient.get('/card/category/clothing/?...') - Request interceptor injects token:
Authorization: Bearer {token} - Backend receives request with token in header
- Response returns
{ success, code, data, message } - API adapter function maps backend response to frontend type (e.g.,
mapBackendOutfit) - Component stores result in React state or updates UI
- Response interceptor handles 401 (token expired) → clears localStorage → redirects to
/login
State Management:
- Token/Auth state: Persisted in
localStorage.auth_token,localStorage.user_role,localStorage.is_superuser; synced to Cookieauth_token - Component state: React useState (shallow, page-scoped)
- Form state: React Hook Form (form-scoped)
- No global state manager (Redux/Zustand not used)
Authentication Token Lifecycle:
- Stored in localStorage + Cookie (7-day expiry) after login
- On every API request, request interceptor reads from localStorage
- If 401 response, token deleted from localStorage, user redirected to
/login - Cookie-based fallback allows middleware to check auth on route entry
Client/Server Component Split
Server Components:
app/layout.tsx— Root layout with metadata only; does NOT render protected content- Most pages are "use client" due to permission checks and hooks
Client Components ("use client"):
- All pages under
app/[module]/— Need React hooks for permission checks, form handling components/dashboard-shell.tsx— Runs permission checks viahasPathPermission()components/sidebar.tsx— UsesusePathname(),useRouter(),useState()for role-based filtering- Feature components — Use useState, API calls, form handling
Key Abstractions
DashboardShell:
- Purpose: Layout wrapper that enforces route-level permissions
- Location:
components/dashboard-shell.tsx - Pattern: Client wrapper checking
hasPathPermission(pathname)on mount; renders either access-denied UI or children - Used by: All page components (wraps page content)
Sidebar:
- Purpose: Dynamic navigation menu filtered by user role
- Location:
components/sidebar.tsx - Pattern: Client component reading
localStorage.user_roleafter mount; filters menu items viahasPermission(module) - Menu structure: Three sections (AI Admin, Content Admin, System Admin) with conditional rendering
- Icon mapping: MenuItem interface + lucide-react icons
API Client Singleton:
- Purpose: Centralized HTTP client with reusable interceptors
- Location:
lib/api/client.tsexportsapiClient(Axios instance) - Pattern: Single instance created at module load; request/response interceptors for auth/error handling
- Interceptors: Request injects token; response handles 401 by clearing token + redirecting
Permission Matrix:
- Purpose: Role → Module[] mapping
- Location:
lib/permissions.tsPERMISSION_MATRIX constant - Pattern: Record<RoleName, PermissionModule[]> with 5 roles × 13 modules
- Functions: getUserRole() → RoleName; hasPermission(module) → boolean; getModuleFromPath(pathname) → module
API Adapter Pattern:
- Purpose: Map backend response schema to frontend type
- Example:
mapBackendOutfit()inlib/api/outfits.ts - Pattern: Backend returns
{ id, name, image_url, rarity_display, ... }; adapter extracts and renames to{ id, name, imageUrl, rarity, ... } - Why: Frontend type contracts differ from backend schema
Entry Points
Browser Entry:
- Location:
app/layout.tsx(Root layout) - Triggers: Initial page load or navigation
- Responsibilities: Renders HTML shell; child routes rendered by App Router
Auth Entry:
- Location:
app/login/page.tsx - Triggers: Unauthenticated users accessing protected routes (redirected by middleware)
- Responsibilities: Render login form, call
emailLogin(), save auth token, redirect to dashboard
Dashboard Entry:
- Location:
app/page.tsx - Triggers: Authenticated users navigating to
/or post-login redirect - Responsibilities: Check auth status, render stats/charts, provide links to submodules
Protected Route Entry (Example):
- Location:
app/outfits/page.tsx - Triggers: User navigates to
/outfits - Responsibilities: Wrap content in DashboardShell (permission check), render OutfitsList component, fetch data
Error Handling
Strategy: Try-catch at API level; Axios interceptor handles 401; UI displays error toast or inline messages.
Patterns:
- API Request Errors: Caught in try-catch, logged, thrown to caller (page/component)
- 401 Unauthorized: Interceptor clears token, redirects to
/login - Form Validation Errors: React Hook Form + Zod pre-submission; validation errors displayed inline
- Permission Denied: DashboardShell renders access-denied UI (no error thrown)
- Mock Data Fallback:
lib/api/client.tsexports mock users/roles; can be used for offline testing
Cross-Cutting Concerns
Logging:
- Verbose Axios logging in request/response interceptors (console.log with emoji prefixes)
- Error logging in catch blocks
Validation:
- React Hook Form + Zod for form fields
- Backend response type-checked against ApiResponse interface
Authentication:
- localStorage.auth_token + Axios Bearer header injection
- Middleware checks Cookie auth_token for route protection
- 401 response triggers token cleanup + redirect
Internationalization:
- Chinese UI text hardcoded in components
- No i18n library used
Architecture analysis: 2026-05-07