9.4 KiB
Raw Blame History

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) and components/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):

  1. User fills login form on app/login/page.tsx
  2. Form submission calls emailLogin(email, password) from lib/api/auth.ts
  3. Axios POSTs to {NEXT_PUBLIC_API_BASE_URL}/v1/admin/login/
  4. Response contains token, role, is_superuser
  5. saveAuthToken(token, isSuperUser, role) stores to localStorage and Cookie
  6. Page redirects to / (dashboard)
  7. Middleware checks Cookie auth_token; on subsequent page loads, Axios request interceptor injects token from localStorage

Protected Page Access Flow:

  1. User navigates to protected route (e.g., /outfits)
  2. Middleware checks cookie; if missing, redirects to /login
  3. Page mounts, DashboardShell component renders
  4. DashboardShell calls hasPathPermission(pathname) at mount
  5. Function checks localStorage.user_role against PERMISSION_MATRIX
  6. If denied, renders access-denied UI; if allowed, renders children
  7. Sidebar calls hasPermission(module) for each menu item (mounted only) to filter visible items
  8. Sidebar also displays current role from localStorage.user_role

Data Fetching Flow (Page → Component → API → Backend):

  1. Page component (e.g., app/outfits/page.tsx) mounts as "use client"
  2. Page renders feature components (e.g., OutfitsList, OutfitsTable)
  3. Feature component calls getOutfits(params) from lib/api/outfits.ts
  4. API function calls apiClient.get('/card/category/clothing/?...')
  5. Request interceptor injects token: Authorization: Bearer {token}
  6. Backend receives request with token in header
  7. Response returns { success, code, data, message }
  8. API adapter function maps backend response to frontend type (e.g., mapBackendOutfit)
  9. Component stores result in React state or updates UI
  10. 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 Cookie auth_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:

  1. Stored in localStorage + Cookie (7-day expiry) after login
  2. On every API request, request interceptor reads from localStorage
  3. If 401 response, token deleted from localStorage, user redirected to /login
  4. 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 via hasPathPermission()
  • components/sidebar.tsx — Uses usePathname(), 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_role after mount; filters menu items via hasPermission(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.ts exports apiClient (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.ts PERMISSION_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() in lib/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.ts exports 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