diff --git a/.gitignore b/.gitignore index e03a68e..121e660 100755 --- a/.gitignore +++ b/.gitignore @@ -302,6 +302,6 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ *.iml - +.junie/* # Docker volumes postgres_data*/ diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index bd21ddc..57dbd68 100755 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -2,6 +2,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { Providers } from "./providers"; +import { AuthProvider } from "@/lib/auth/AuthContext"; +import { AuthInitializer } from "@/components/auth"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -58,7 +60,10 @@ export default function RootLayout({ - {children} + + + {children} + ); diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx index deba5ac..15314f8 100644 --- a/frontend/src/app/providers.tsx +++ b/frontend/src/app/providers.tsx @@ -3,7 +3,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { lazy, Suspense, useState } from 'react'; import { ThemeProvider } from '@/components/theme'; -import { AuthInitializer } from '@/components/auth'; // Lazy load devtools - only in local development (not in Docker), never in production // Set NEXT_PUBLIC_ENABLE_DEVTOOLS=true in .env.local to enable @@ -39,7 +38,6 @@ export function Providers({ children }: { children: React.ReactNode }) { return ( - {children} {ReactQueryDevtools && ( diff --git a/frontend/src/lib/auth/AuthContext.tsx b/frontend/src/lib/auth/AuthContext.tsx new file mode 100644 index 0000000..649cd41 --- /dev/null +++ b/frontend/src/lib/auth/AuthContext.tsx @@ -0,0 +1,149 @@ +/** + * 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"); + } + + // 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(); +} diff --git a/frontend/src/lib/stores/index.ts b/frontend/src/lib/stores/index.ts index 210d879..04e47ff 100755 --- a/frontend/src/lib/stores/index.ts +++ b/frontend/src/lib/stores/index.ts @@ -2,3 +2,6 @@ // Examples: authStore, uiStore, etc. export { useAuthStore, initializeAuth, type User } from './authStore'; + +// Authentication Context (DI wrapper for auth store) +export { useAuth, AuthProvider } from '../auth/AuthContext';