204 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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*