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