/** * AUTH HOOKS - REFERENCE IMPLEMENTATION (React) * * @description Hooks personalizados para autenticación en aplicaciones React. * Incluye manejo de estado, persistencia, refresh automático y permisos. * * @usage * ```tsx * import { useAuth, useSession, usePermissions } from '@/hooks/auth'; * * function MyComponent() { * const { user, login, logout, isAuthenticated } = useAuth(); * const { session, refreshSession } = useSession(); * const { can, hasRole } = usePermissions(); * * return ( *
* {isAuthenticated ? ( *

Welcome {user.email}

* ) : ( * * )} *
* ); * } * ``` * * @dependencies * - react >= 18.0.0 * - @tanstack/react-query (opcional, para caching) * - zustand o context (para estado global) * * @origin Patrón base para aplicaciones React con autenticación */ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; // ============ TIPOS ============ interface User { id: string; email: string; role: string; firstName?: string; lastName?: string; avatar?: string; } interface AuthTokens { accessToken: string; refreshToken: string; } interface LoginCredentials { email: string; password: string; } interface RegisterData extends LoginCredentials { firstName?: string; lastName?: string; } interface AuthState { user: User | null; tokens: AuthTokens | null; isAuthenticated: boolean; isLoading: boolean; error: string | null; } interface Permission { resource: string; action: string; // 'create' | 'read' | 'update' | 'delete' } // ============ CONFIGURACIÓN ============ const AUTH_CONFIG = { API_BASE_URL: process.env.REACT_APP_API_URL || 'http://localhost:3000', STORAGE_KEY: 'auth_tokens', REFRESH_INTERVAL: 14 * 60 * 1000, // 14 minutos (antes de expirar el access token) TOKEN_EXPIRY_BUFFER: 60 * 1000, // 1 minuto de buffer }; // ============ UTILIDADES DE STORAGE ============ const storage = { get: (): AuthTokens | null => { const data = localStorage.getItem(AUTH_CONFIG.STORAGE_KEY); return data ? JSON.parse(data) : null; }, set: (tokens: AuthTokens): void => { localStorage.setItem(AUTH_CONFIG.STORAGE_KEY, JSON.stringify(tokens)); }, clear: (): void => { localStorage.removeItem(AUTH_CONFIG.STORAGE_KEY); }, }; // ============ API CLIENT ============ class AuthAPI { private baseURL = AUTH_CONFIG.API_BASE_URL; private async request( endpoint: string, options: RequestInit = {}, ): Promise { const url = `${this.baseURL}${endpoint}`; const headers = { 'Content-Type': 'application/json', ...options.headers, }; const response = await fetch(url, { ...options, headers }); if (!response.ok) { const error = await response.json().catch(() => ({ message: 'Request failed' })); throw new Error(error.message || 'Request failed'); } return response.json(); } async login(credentials: LoginCredentials): Promise<{ user: User; tokens: AuthTokens }> { return this.request('/auth/login', { method: 'POST', body: JSON.stringify(credentials), }); } async register(data: RegisterData): Promise<{ user: User; tokens: AuthTokens }> { return this.request('/auth/register', { method: 'POST', body: JSON.stringify(data), }); } async logout(accessToken: string): Promise { return this.request('/auth/logout', { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` }, }); } async refreshToken(refreshToken: string): Promise { return this.request('/auth/refresh', { method: 'POST', body: JSON.stringify({ refreshToken }), }); } async getProfile(accessToken: string): Promise { return this.request('/auth/profile', { headers: { Authorization: `Bearer ${accessToken}` }, }); } } const authAPI = new AuthAPI(); // ============ HOOK: useAuth ============ /** * Hook principal de autenticación * * @example * ```tsx * const { user, login, logout, register, isAuthenticated, isLoading } = useAuth(); * * const handleLogin = async () => { * try { * await login({ email: 'user@example.com', password: 'password' }); * navigate('/dashboard'); * } catch (error) { * console.error('Login failed:', error); * } * }; * ``` */ export function useAuth() { const [state, setState] = useState({ user: null, tokens: null, isAuthenticated: false, isLoading: true, error: null, }); const navigate = useNavigate(); // Cargar usuario inicial desde tokens almacenados useEffect(() => { const initAuth = async () => { const tokens = storage.get(); if (!tokens) { setState((prev) => ({ ...prev, isLoading: false })); return; } try { const user = await authAPI.getProfile(tokens.accessToken); setState({ user, tokens, isAuthenticated: true, isLoading: false, error: null, }); } catch (error) { // Token inválido o expirado - intentar refresh try { const newTokens = await authAPI.refreshToken(tokens.refreshToken); storage.set(newTokens); const user = await authAPI.getProfile(newTokens.accessToken); setState({ user, tokens: newTokens, isAuthenticated: true, isLoading: false, error: null, }); } catch (refreshError) { // Refresh falló - limpiar todo storage.clear(); setState({ user: null, tokens: null, isAuthenticated: false, isLoading: false, error: null, }); } } }; initAuth(); }, []); const login = useCallback(async (credentials: LoginCredentials) => { setState((prev) => ({ ...prev, isLoading: true, error: null })); try { const { user, tokens } = await authAPI.login(credentials); storage.set(tokens); setState({ user, tokens, isAuthenticated: true, isLoading: false, error: null, }); } catch (error) { setState((prev) => ({ ...prev, isLoading: false, error: error instanceof Error ? error.message : 'Login failed', })); throw error; } }, []); const register = useCallback(async (data: RegisterData) => { setState((prev) => ({ ...prev, isLoading: true, error: null })); try { const { user, tokens } = await authAPI.register(data); storage.set(tokens); setState({ user, tokens, isAuthenticated: true, isLoading: false, error: null, }); } catch (error) { setState((prev) => ({ ...prev, isLoading: false, error: error instanceof Error ? error.message : 'Registration failed', })); throw error; } }, []); const logout = useCallback(async () => { if (state.tokens) { try { await authAPI.logout(state.tokens.accessToken); } catch (error) { // Ignorar error de logout - limpiar estado de todos modos console.error('Logout error:', error); } } storage.clear(); setState({ user: null, tokens: null, isAuthenticated: false, isLoading: false, error: null, }); navigate('/login'); }, [state.tokens, navigate]); const updateUser = useCallback((updates: Partial) => { setState((prev) => ({ ...prev, user: prev.user ? { ...prev.user, ...updates } : null, })); }, []); return { user: state.user, tokens: state.tokens, isAuthenticated: state.isAuthenticated, isLoading: state.isLoading, error: state.error, login, register, logout, updateUser, }; } // ============ HOOK: useSession ============ /** * Hook para gestión de sesión y refresh automático de tokens * * @example * ```tsx * const { session, isValid, expiresIn, refreshSession } = useSession(); * * useEffect(() => { * if (expiresIn < 60000) { // Menos de 1 minuto * refreshSession(); * } * }, [expiresIn]); * ``` */ export function useSession() { const [tokens, setTokens] = useState(storage.get()); const [lastRefresh, setLastRefresh] = useState(null); // Refresh automático useEffect(() => { if (!tokens) return; const interval = setInterval(async () => { try { const newTokens = await authAPI.refreshToken(tokens.refreshToken); storage.set(newTokens); setTokens(newTokens); setLastRefresh(new Date()); } catch (error) { console.error('Auto-refresh failed:', error); // Si el refresh falla, el usuario será redirigido al login en el próximo request } }, AUTH_CONFIG.REFRESH_INTERVAL); return () => clearInterval(interval); }, [tokens]); const refreshSession = useCallback(async () => { if (!tokens) { throw new Error('No active session'); } try { const newTokens = await authAPI.refreshToken(tokens.refreshToken); storage.set(newTokens); setTokens(newTokens); setLastRefresh(new Date()); return newTokens; } catch (error) { storage.clear(); setTokens(null); throw error; } }, [tokens]); const expiresIn = useMemo(() => { if (!tokens || !lastRefresh) return null; const elapsed = Date.now() - lastRefresh.getTime(); return AUTH_CONFIG.REFRESH_INTERVAL - elapsed; }, [tokens, lastRefresh]); return { session: tokens, isValid: !!tokens, expiresIn, lastRefresh, refreshSession, }; } // ============ HOOK: usePermissions ============ /** * Hook para control de permisos y roles (RBAC) * * @example * ```tsx * const { can, hasRole, hasAnyRole, hasAllRoles } = usePermissions(); * * if (can('users', 'delete')) { * return ; * } * * if (hasRole('admin')) { * return ; * } * ``` */ export function usePermissions() { const { user } = useAuth(); // Mapeo de roles a permisos (adaptar según proyecto) const rolePermissions: Record = { admin: [ { resource: '*', action: '*' }, // Admin tiene todos los permisos ], moderator: [ { resource: 'users', action: 'read' }, { resource: 'users', action: 'update' }, { resource: 'posts', action: '*' }, { resource: 'comments', action: '*' }, ], user: [ { resource: 'profile', action: 'read' }, { resource: 'profile', action: 'update' }, { resource: 'posts', action: 'create' }, { resource: 'posts', action: 'read' }, ], }; const getUserPermissions = useCallback((): Permission[] => { if (!user) return []; return rolePermissions[user.role] || []; }, [user]); const can = useCallback( (resource: string, action: string): boolean => { const permissions = getUserPermissions(); return permissions.some( (perm) => (perm.resource === '*' || perm.resource === resource) && (perm.action === '*' || perm.action === action), ); }, [getUserPermissions], ); const hasRole = useCallback( (role: string): boolean => { return user?.role === role; }, [user], ); const hasAnyRole = useCallback( (roles: string[]): boolean => { return roles.some((role) => user?.role === role); }, [user], ); const hasAllRoles = useCallback( (roles: string[]): boolean => { // En un sistema simple con un solo role por usuario, esto solo funciona con un role // En sistemas complejos con múltiples roles, adaptar lógica return roles.length === 1 && user?.role === roles[0]; }, [user], ); return { can, hasRole, hasAnyRole, hasAllRoles, permissions: getUserPermissions(), }; } // ============ COMPONENTES DE UTILIDAD ============ /** * Componente de protección por permisos * * @example * ```tsx * * * * ``` */ export function RequirePermission({ resource, action, fallback = null, children, }: { resource: string; action: string; fallback?: React.ReactNode; children: React.ReactNode; }) { const { can } = usePermissions(); if (!can(resource, action)) { return <>{fallback}; } return <>{children}; } /** * Componente de protección por rol * * @example * ```tsx * * * * * * * * ``` */ export function RequireRole({ role, roles, requireAll = false, fallback = null, children, }: { role?: string; roles?: string[]; requireAll?: boolean; fallback?: React.ReactNode; children: React.ReactNode; }) { const { hasRole, hasAnyRole, hasAllRoles } = usePermissions(); let hasAccess = false; if (role) { hasAccess = hasRole(role); } else if (roles) { hasAccess = requireAll ? hasAllRoles(roles) : hasAnyRole(roles); } if (!hasAccess) { return <>{fallback}; } return <>{children}; } /** * HOC para proteger rutas * * @example * ```tsx * const ProtectedDashboard = withAuth(Dashboard); * const AdminPanel = withAuth(AdminPanelComponent, { role: 'admin' }); * ``` */ export function withAuth

( Component: React.ComponentType

, options?: { role?: string; roles?: string[]; requireAll?: boolean; redirectTo?: string; }, ) { return function WithAuthComponent(props: P) { const { isAuthenticated, isLoading } = useAuth(); const { hasRole, hasAnyRole, hasAllRoles } = usePermissions(); const navigate = useNavigate(); useEffect(() => { if (isLoading) return; if (!isAuthenticated) { navigate(options?.redirectTo || '/login'); return; } if (options?.role && !hasRole(options.role)) { navigate('/unauthorized'); return; } if (options?.roles) { const hasAccess = options.requireAll ? hasAllRoles(options.roles) : hasAnyRole(options.roles); if (!hasAccess) { navigate('/unauthorized'); } } }, [isAuthenticated, isLoading, navigate, hasRole, hasAnyRole, hasAllRoles]); if (isLoading) { return

Loading...
; } if (!isAuthenticated) { return null; } return ; }; } // ============ EXPORTS ============ export type { User, AuthTokens, LoginCredentials, RegisterData, AuthState, Permission, }; export { authAPI, storage };