diff --git a/AUTH_CONTEXT_MIGRATION_PLAN.md b/AUTH_CONTEXT_MIGRATION_PLAN.md deleted file mode 100644 index d744319..0000000 --- a/AUTH_CONTEXT_MIGRATION_PLAN.md +++ /dev/null @@ -1,2817 +0,0 @@ -# Authentication Context DI Migration Plan - -**Version**: 1.0 -**Date**: 2025-11-03 -**Objective**: Migrate authentication system from direct Zustand singleton usage to Context-based Dependency Injection pattern for full testability while maintaining security and performance. - ---- - -## Table of Contents - -1. [Executive Summary](#executive-summary) -2. [Context & Problem Analysis](#context--problem-analysis) -3. [Solution Architecture](#solution-architecture) -4. [Implementation Phases](#implementation-phases) -5. [Testing Strategy](#testing-strategy) -6. [Rollback Plan](#rollback-plan) -7. [Success Criteria](#success-criteria) - ---- - -## Executive Summary - -### Current State -- **Authentication**: Zustand singleton store with AES-GCM encryption -- **Security**: Production-grade (JWT validation, session tracking, encrypted storage) -- **Performance**: Excellent (singleton refresh pattern, 98.38% test coverage) -- **Testability**: Poor (E2E tests cannot mock auth state) - -### Target State -- **Authentication**: Zustand store wrapped in React Context for DI -- **Security**: Unchanged (all security logic preserved) -- **Performance**: Unchanged (same runtime characteristics) -- **Testability**: Excellent (E2E tests can inject mock stores) - -### Impact -- **Files Modified**: 13 files (8 source, 5 tests) -- **Files Created**: 2 new files -- **Breaking Changes**: None (internal refactor only) -- **Test Coverage**: Maintained at ≥98.38% - -### Success Metrics -- 100% unit test pass rate -- 100% E2E test pass rate (86 total tests) -- Zero console errors -- Zero performance regression -- All manual test scenarios pass - ---- - -## Phase 1 Lessons Learned (CRITICAL - READ FIRST) - -**Phase 1 Status**: ✅ **COMPLETED** - All issues resolved - -### Key Implementation Insights - -#### 1. useAuth Hook Must Call Zustand Hook Internally - -**❌ WRONG (Original Plan)**: -```typescript -export function useAuth() { - const context = useContext(AuthContext); - return context; // Returns the hook function - VIOLATES React Rules of Hooks! -} -``` - -**✅ CORRECT (Implemented)**: -```typescript -export function useAuth(): AuthState; -export function useAuth(selector: (state: AuthState) => T): T; -export function useAuth(selector?: (state: AuthState) => T): AuthState | T { - const storeHook = useContext(AuthContext); - if (!storeHook) { - throw new Error("useAuth must be used within AuthProvider"); - } - // CRITICAL: Call the hook internally - return selector ? storeHook(selector) : storeHook(); -} -``` - -**Why This Matters**: -- ✅ Enables `const { user } = useAuth()` pattern (simple, idiomatic) -- ✅ Also supports `const user = useAuth(s => s.user)` pattern (optimized) -- ✅ Follows React Rules of Hooks (hook called at component top level) -- ❌ Without this, components would need `const { user } = useAuth()()` (wrong!) - -#### 2. Provider Placement Architecture - -**Correct Structure**: -``` -layout.tsx: - ← Provides DI layer - ← Loads auth from storage (needs AuthProvider) - ← Other providers (Theme, Query) - {children} - - -``` - -**Why This Order**: -- AuthProvider must wrap AuthInitializer (AuthInitializer uses auth state) -- AuthProvider should wrap Providers (auth available everywhere) -- Keep provider tree shallow (performance) - -#### 3. Type Safety with Explicit AuthState Interface - -**✅ DO**: Define explicit AuthState interface matching Zustand store: -```typescript -interface AuthState { - user: User | null; - accessToken: string | null; - // ... all properties explicitly typed -} -``` - -**Benefits**: -- IDE autocomplete works perfectly -- Type errors caught at compile time -- Self-documenting code -- Easier to maintain - -#### 4. Comprehensive JSDoc Documentation - -**Pattern to Follow**: -```typescript -/** - * [Component/Function Name] - [One-line purpose] - * - * [Detailed description including:] - * - What it does - * - When to use it - * - How it works - * - * @param {Type} paramName - Parameter description - * @throws {ErrorType} When error occurs - * @returns {Type} Return value description - * - * @example - * ```tsx - * // Concrete usage example - * const { user } = useAuth(); - * ``` - */ -``` - -**Include Examples For**: -- Different usage patterns (with/without selectors) -- Common mistakes to avoid -- Testing scenarios - -#### 5. Barrel Exports for Clean Imports - -**DO**: Add to barrel files (`src/lib/stores/index.ts`): -```typescript -export { useAuth, AuthProvider } from '../auth/AuthContext'; -``` - -**Benefits**: -- Consistent import paths: `import { useAuth } from '@/lib/stores'` -- Easy to refactor internal structure -- Clear public API - -### Critical Mistakes to Avoid - -1. **❌ Don't return hook function from useAuth** - - Returns function, not state - - Violates React Rules of Hooks - - Breaks in Phase 2+ - -2. **❌ Don't nest AuthProvider inside Providers** - - AuthInitializer won't have access to auth - - Wrong dependency order - -3. **❌ Don't forget barrel exports** - - Inconsistent import paths - - Harder to maintain - -4. **❌ Don't skip documentation** - - Future developers won't understand usage - - Leads to incorrect implementations - -5. **❌ Don't use implicit types** - - Harder to debug type issues - - Worse IDE support - -### Verification Checklist (Always Run) - -After any changes: -- [ ] `npm run type-check` - Must pass with 0 errors -- [ ] `npm run dev` - App must start without errors -- [ ] Browser console - Must be clean (no errors/warnings) -- [ ] Test actual usage - Navigate to protected routes -- [ ] Check React DevTools - Verify provider tree structure - -### Performance Considerations - -**Polymorphic useAuth Hook**: -- ✅ No performance overhead (hook called once per component) -- ✅ Selector pattern available for optimization -- ✅ Same performance as direct Zustand usage - -**Context Provider**: -- ✅ Stable value (doesn't change unless store changes) -- ✅ No extra re-renders -- ✅ Negligible memory overhead - ---- - -## Context & Problem Analysis - -### Current Architecture - -``` -Component → useAuthStore (direct import) → Zustand singleton → storage.ts → crypto.ts - ↑ - (Not mockable in E2E) -``` - -**Files using `useAuthStore`** (13 total): - -**Components** (3): -- `src/components/auth/AuthGuard.tsx:53` - Route protection -- `src/components/auth/AuthInitializer.tsx:32` - Startup auth loading -- `src/components/layout/Header.tsx:70` - User display - -**Hooks** (2): -- `src/lib/api/hooks/useAuth.ts` - 12+ locations (login, logout, state checks) -- `src/lib/api/hooks/useUser.ts:34` - Profile updates - -**Utilities** (1): -- `src/lib/api/client.ts:36-38` - Token interceptors (dynamic import) - -**Core Store** (2): -- `src/lib/stores/authStore.ts` - Store definition -- `src/lib/stores/index.ts` - Export barrel - -**Tests** (5): -- `tests/components/layout/Header.test.tsx` -- `tests/components/auth/AuthInitializer.test.tsx` -- `tests/lib/stores/authStore.test.ts` -- `tests/lib/api/hooks/useUser.test.tsx` -- `tests/app/(authenticated)/settings/profile/page.test.tsx` - -### Core Problem - -**E2E tests cannot establish authenticated state** because: - -1. **Singleton Pattern**: `export const useAuthStore = create(...)` creates module-level singleton -2. **No Injection Point**: Components import and call `useAuthStore()` directly -3. **Encryption Barrier**: Tokens require AES-GCM encryption setup (key + IV + ciphertext) -4. **Race Conditions**: `AuthInitializer` runs on page load, overwrites test mocks - -**Result**: 45 settings E2E tests fail, cannot test authenticated flows end-to-end. - -### Why Context DI is the Right Solution - -**Alternatives Considered**: -- ❌ Test-mode flag to disable encryption (hack, test-only code in production) -- ❌ Backend seeding (requires running backend, slow, complex) -- ❌ Cookie-based auth (major architecture change, not compatible with current JWT flow) - -**Why Context Wins**: -- ✅ Industry-standard React pattern for DI -- ✅ Zero changes to business logic or security -- ✅ Clean separation: Context handles injection, Zustand handles state -- ✅ Testable at both unit and E2E levels -- ✅ Future-proof (easy to add auth events, middleware, logging) -- ✅ No performance overhead (Context value is stable) - ---- - -## Solution Architecture - -### Target Architecture - -``` -Component → useAuth() hook → AuthContext → Zustand store instance → storage.ts → crypto.ts - ↓ - Provider wrapper (injectable) - ↓ - Production: Real store | Tests: Mock store -``` - -### Design Principles - -1. **Thin Context Layer**: Context only provides dependency injection, no business logic -2. **Zustand for State**: All state management stays in Zustand (no duplicated state) -3. **Backward Compatible**: Internal refactor only, no API changes -4. **Type Safe**: Context interface exactly matches Zustand store interface -5. **Performance**: Context value is stable (no unnecessary re-renders) - -### Key Components - -#### 1. AuthContext Provider -- Wraps entire app at root layout -- Accepts optional `store` prop for testing -- Falls back to real Zustand singleton in production -- Checks for E2E test store in `window.__TEST_AUTH_STORE__` - -#### 2. useAuth Hook -- Replaces direct `useAuthStore` calls in components -- Returns store instance from Context -- Type-safe (infers exact store shape) -- Throws error if used outside Provider - -#### 3. Store Access Patterns - -**For Components (rendering auth state)**: -```typescript -import { useAuth } from '@/lib/auth/AuthContext'; - -function MyComponent() { - const { user, isAuthenticated } = useAuth(); - return
{user?.firstName}
; -} -``` - -**For Mutation Callbacks (updating auth state)**: -```typescript -import { useAuthStore } from '@/lib/stores/authStore'; - -// In React Query mutation -mutationFn: async (data) => { - const response = await api.call(data); - const setAuth = useAuthStore.getState().setAuth; - await setAuth(response.user, response.token); -} -``` - -**Rationale**: Mutation callbacks run outside React render cycle, don't need Context. Using `getState()` directly is cleaner and avoids unnecessary hook rules. - -#### 4. E2E Test Integration -```typescript -// Before page load, inject mock store -await page.addInitScript((mockStore) => { - (window as any).__TEST_AUTH_STORE__ = mockStore; -}, MOCK_AUTHENTICATED_STORE); - -// AuthContext checks window.__TEST_AUTH_STORE__ and uses it -``` - ---- - -## Implementation Phases - -### Phase 1: Foundation - Create Context Layer - -#### Task 1.1: Create AuthContext Module -**File**: `src/lib/auth/AuthContext.tsx` (NEW) - -**CRITICAL**: The `useAuth` hook must call the Zustand hook internally to follow React's Rules of Hooks. Do NOT return the hook function itself. - -```typescript -/** - * Authentication Context - Dependency Injection Wrapper for Auth Store - * - * Provides a thin Context layer over Zustand auth store to enable: - * - Test isolation (inject mock stores) - * - E2E testing without backend - * - Clean architecture (DI pattern) - * - * Design: Context handles dependency injection, Zustand handles state management - */ - -"use client"; - -import { createContext, useContext } from "react"; -import type { ReactNode } from "react"; -import { useAuthStore as useAuthStoreImpl } from "@/lib/stores/authStore"; -import type { User } from "@/lib/stores/authStore"; - -/** - * Authentication state shape - * Matches the Zustand store interface exactly - */ -interface AuthState { - // State - user: User | null; - accessToken: string | null; - refreshToken: string | null; - isAuthenticated: boolean; - isLoading: boolean; - tokenExpiresAt: number | null; - - // Actions - setAuth: (user: User, accessToken: string, refreshToken: string, expiresIn?: number) => Promise; - setTokens: (accessToken: string, refreshToken: string, expiresIn?: number) => Promise; - setUser: (user: User) => void; - clearAuth: () => Promise; - loadAuthFromStorage: () => Promise; - isTokenExpired: () => boolean; -} - -/** - * Type of the Zustand hook function - * Used for Context storage and test injection - */ -type AuthStoreHook = typeof useAuthStoreImpl; - -/** - * Global window extension for E2E test injection - * E2E tests can set window.__TEST_AUTH_STORE__ before navigation - */ -declare global { - interface Window { - __TEST_AUTH_STORE__?: AuthStoreHook; - } -} - -const AuthContext = createContext(null); - -interface AuthProviderProps { - children: ReactNode; - /** - * Optional store override for testing - * Used in unit tests to inject mock store - */ - store?: AuthStoreHook; -} - -/** - * Authentication Context Provider - * - * Wraps Zustand auth store in React Context for dependency injection. - * Enables test isolation by allowing mock stores to be injected via: - * 1. `store` prop (unit tests) - * 2. `window.__TEST_AUTH_STORE__` (E2E tests) - * 3. Production singleton (default) - * - * @example - * ```tsx - * // In root layout - * - * - * - * - * // In unit tests - * - * - * - * - * // In E2E tests (before navigation) - * window.__TEST_AUTH_STORE__ = mockAuthStoreHook; - * ``` - */ -export function AuthProvider({ children, store }: AuthProviderProps) { - // Check for E2E test store injection (SSR-safe) - const testStore = - typeof window !== "undefined" && window.__TEST_AUTH_STORE__ - ? window.__TEST_AUTH_STORE__ - : null; - - // Priority: explicit prop > E2E test store > production singleton - const authStore = store ?? testStore ?? useAuthStoreImpl; - - return {children}; -} - -/** - * Hook to access authentication state and actions - * - * Supports both full state access and selector patterns for performance optimization. - * Must be used within AuthProvider. - * - * @throws {Error} If used outside of AuthProvider - * - * @example - * ```tsx - * // Full state access (simpler, re-renders on any state change) - * function MyComponent() { - * const { user, isAuthenticated } = useAuth(); - * return
{user?.first_name}
; - * } - * - * // Selector pattern (optimized, re-renders only when selected value changes) - * function UserName() { - * const user = useAuth(state => state.user); - * return {user?.first_name}; - * } - * - * // In mutation callbacks (outside React render) - * const handleLogin = async (data) => { - * const response = await loginAPI(data); - * // Use getState() directly for mutations (see useAuth.ts hooks) - * const setAuth = useAuthStore.getState().setAuth; - * await setAuth(response.user, response.token); - * }; - * ``` - */ -export function useAuth(): AuthState; -export function useAuth(selector: (state: AuthState) => T): T; -export function useAuth(selector?: (state: AuthState) => T): AuthState | T { - const storeHook = useContext(AuthContext); - - if (!storeHook) { - throw new Error("useAuth must be used within AuthProvider"); - } - - // CRITICAL: Call the Zustand hook internally (follows React Rules of Hooks) - // This is the key difference from returning the hook function itself - return selector ? storeHook(selector) : storeHook(); -} -``` - -**Key Implementation Details**: -1. **Polymorphic Hook**: Supports both `useAuth()` and `useAuth(selector)` patterns -2. **Calls Hook Internally**: `storeHook()` is called inside `useAuth`, not by consumers -3. **Type Safety**: `AuthState` interface matches Zustand store exactly -4. **Window Global**: Type-safe extension for E2E test injection - -**Verification**: -- Run: `npm run type-check` -- Verify: `AuthState` interface matches all Zustand store properties -- Check: No circular import warnings -- Verify: Polymorphic overloads work correctly - -**Success Criteria**: -- [x] File created with correct implementation -- [x] TypeScript compiles without errors -- [x] Type inference works correctly -- [x] Hook calls Zustand hook internally (not returns it) - ---- - -#### Task 1.2: Wrap Application Root -**File**: `src/app/layout.tsx` (MODIFY) - -**Also**: `src/app/providers.tsx` (MODIFY) - Remove AuthInitializer from here -**Also**: `src/lib/stores/index.ts` (MODIFY) - Add barrel exports - -**Step 1: Add imports to layout.tsx**: -```typescript -// At the top of layout.tsx, add these imports: -import { AuthProvider } from "@/lib/auth/AuthContext"; -import { AuthInitializer } from "@/components/auth"; -``` - -**Step 2: Wrap body content with AuthProvider**: -```typescript -export default function RootLayout({ children }: { children: ReactNode }) { - return ( - - - {/* Theme initialization script stays here */} -