204 lines
9.4 KiB
Markdown
204 lines
9.4 KiB
Markdown
# 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*
|