import * as React from 'react'; import { createContext, useContext, useCallback, useMemo, useEffect, useState } from 'react'; import type { User, AuthState, LoginCredentials } from './types'; const TOKEN_STORAGE_KEY = 'auth_token'; const USER_STORAGE_KEY = 'auth_user'; /** * Authentication context value. */ export interface AuthContextValue extends AuthState { /** Log in with credentials */ login: (credentials: LoginCredentials) => Promise; /** Log in with a token directly */ loginWithToken: (token: string, user?: User) => void; /** Log out the current user */ logout: () => void; /** Get the current access token */ getToken: () => string | null; /** Check if user has a specific role */ hasRole: (role: string) => boolean; /** Check if user has a specific scope */ hasScope: (scope: string) => boolean; } const AuthContext = createContext(null); /** * Auth provider configuration. */ export interface AuthProviderProps { children: React.ReactNode; /** API endpoint for login */ loginUrl?: string; /** API endpoint for logout */ logoutUrl?: string; /** API endpoint for fetching current user */ userUrl?: string; /** Custom login handler */ onLogin?: (credentials: LoginCredentials) => Promise<{ token: string; user: User }>; /** Custom logout handler */ onLogout?: () => Promise; /** Storage type for persisting auth state */ storage?: 'localStorage' | 'sessionStorage' | 'none'; } /** * AuthProvider manages authentication state and provides auth methods. * * @example * // Basic usage * * * * * @example * // With custom handlers * { * const res = await myAuthService.login(creds); * return { token: res.token, user: res.user }; * }} * > * * */ export function AuthProvider({ children, loginUrl = '/api/auth/login', logoutUrl = '/api/auth/logout', userUrl = '/api/auth/me', onLogin, onLogout, storage = 'localStorage', }: AuthProviderProps) { const [state, setState] = useState({ user: null, isLoading: true, isAuthenticated: false, error: null, }); // Get storage implementation const getStorage = useCallback(() => { if (storage === 'none') return null; return storage === 'sessionStorage' ? sessionStorage : localStorage; }, [storage]); // Initialize auth state from storage useEffect(() => { const store = getStorage(); if (!store) { setState((s) => ({ ...s, isLoading: false })); return; } const token = store.getItem(TOKEN_STORAGE_KEY); const userJson = store.getItem(USER_STORAGE_KEY); if (token && userJson) { try { const user = JSON.parse(userJson) as User; setState({ user, isLoading: false, isAuthenticated: true, error: null, }); } catch { // Invalid stored data, clear it store.removeItem(TOKEN_STORAGE_KEY); store.removeItem(USER_STORAGE_KEY); setState((s) => ({ ...s, isLoading: false })); } } else { setState((s) => ({ ...s, isLoading: false })); } }, [getStorage]); // Login with credentials const login = useCallback( async (credentials: LoginCredentials) => { setState((s) => ({ ...s, isLoading: true, error: null })); try { let token: string; let user: User; if (onLogin) { // Use custom login handler const result = await onLogin(credentials); token = result.token; user = result.user; } else { // Use default API login const response = await fetch(loginUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials), }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.message || 'Login failed'); } const data = await response.json(); token = data.data?.token || data.token; user = data.data?.user || data.user; } // Store token and user const store = getStorage(); if (store) { store.setItem(TOKEN_STORAGE_KEY, token); store.setItem(USER_STORAGE_KEY, JSON.stringify(user)); } setState({ user, isLoading: false, isAuthenticated: true, error: null, }); } catch (error) { setState({ user: null, isLoading: false, isAuthenticated: false, error: error instanceof Error ? error : new Error('Login failed'), }); throw error; } }, [loginUrl, onLogin, getStorage] ); // Login with token directly const loginWithToken = useCallback( (token: string, user?: User) => { const store = getStorage(); if (store) { store.setItem(TOKEN_STORAGE_KEY, token); if (user) { store.setItem(USER_STORAGE_KEY, JSON.stringify(user)); } } setState({ user: user || null, isLoading: false, isAuthenticated: true, error: null, }); }, [getStorage] ); // Logout const logout = useCallback(async () => { try { if (onLogout) { await onLogout(); } else if (logoutUrl) { await fetch(logoutUrl, { method: 'POST' }).catch(() => {}); } } finally { const store = getStorage(); if (store) { store.removeItem(TOKEN_STORAGE_KEY); store.removeItem(USER_STORAGE_KEY); } setState({ user: null, isLoading: false, isAuthenticated: false, error: null, }); } }, [logoutUrl, onLogout, getStorage]); // Get token const getToken = useCallback(() => { const store = getStorage(); return store ? store.getItem(TOKEN_STORAGE_KEY) : null; }, [getStorage]); // Role check const hasRole = useCallback( (role: string) => { return state.user?.roles?.includes(role) ?? false; }, [state.user] ); // Scope check const hasScope = useCallback( (scope: string) => { return state.user?.scopes?.includes(scope) ?? false; }, [state.user] ); const value = useMemo( (): AuthContextValue => ({ ...state, login, loginWithToken, logout, getToken, hasRole, hasScope, }), [state, login, loginWithToken, logout, getToken, hasRole, hasScope] ); return {children}; } /** * Hook to access authentication state and methods. * * @example * function Profile() { * const { user, logout, isAuthenticated } = useAuth(); * * if (!isAuthenticated) { * return ; * } * * return ( *
*

Welcome, {user?.name}

* *
* ); * } */ export function useAuth(): AuthContextValue { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }