Introduce AuthContext and refactor layout for dependency injection

- Added `AuthContext` as a dependency injection wrapper over the Zustand auth store to support test isolation, E2E testability, and clean architecture patterns.
- Updated `layout.tsx` to utilize `AuthProvider` and initialize authentication context.
- Removed redundant `AuthInitializer` from `providers.tsx`.
- Enhanced modularity and testability by decoupling authentication context from direct store dependency.
This commit is contained in:
Felipe Cardoso
2025-11-03 11:33:39 +01:00
parent 01b406bca7
commit 0cba8ea62a
5 changed files with 159 additions and 4 deletions

2
.gitignore vendored
View File

@@ -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*/

View File

@@ -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({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Providers>{children}</Providers>
<AuthProvider>
<AuthInitializer />
<Providers>{children}</Providers>
</AuthProvider>
</body>
</html>
);

View File

@@ -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 (
<ThemeProvider>
<QueryClientProvider client={queryClient}>
<AuthInitializer />
{children}
{ReactQueryDevtools && (
<Suspense fallback={null}>

View File

@@ -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<void>;
setTokens: (accessToken: string, refreshToken: string, expiresIn?: number) => Promise<void>;
setUser: (user: User) => void;
clearAuth: () => Promise<void>;
loadAuthFromStorage: () => Promise<void>;
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<AuthStoreHook | null>(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
* <AuthProvider>
* <App />
* </AuthProvider>
*
* // In unit tests
* <AuthProvider store={mockStore}>
* <ComponentUnderTest />
* </AuthProvider>
*
* // 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 <AuthContext.Provider value={authStore}>{children}</AuthContext.Provider>;
}
/**
* 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 <div>{user?.first_name}</div>;
* }
*
* // Selector pattern (optimized, re-renders only when selected value changes)
* function UserName() {
* const user = useAuth(state => state.user);
* return <span>{user?.first_name}</span>;
* }
*
* // 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<T>(selector: (state: AuthState) => T): T;
export function useAuth<T>(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();
}

View File

@@ -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';