/**
* 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 };