forked from cardosofelipe/fast-next-template
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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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*/
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}>
|
||||
|
||||
149
frontend/src/lib/auth/AuthContext.tsx
Normal file
149
frontend/src/lib/auth/AuthContext.tsx
Normal 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();
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user