Sistema NEXUS v3.4 migrado con: Estructura principal: - core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles) - core/catalog: Catalogo de funcionalidades reutilizables - shared/knowledge-base: Base de conocimiento compartida - devtools/scripts: Herramientas de desarrollo - control-plane/registries: Control de servicios y CI/CD - orchestration/: Configuracion de orquestacion de agentes Proyectos incluidos (11): - gamilit (submodule -> GitHub) - trading-platform (OrbiquanTIA) - erp-suite con 5 verticales: - erp-core, construccion, vidrio-templado - mecanicas-diesel, retail, clinicas - betting-analytics - inmobiliaria-analytics - platform_marketing_content - pos-micro, erp-basico Configuracion: - .gitignore completo para Node.js/Python/Docker - gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git) - Sistema de puertos estandarizado (3005-3199) Generated with NEXUS v3.4 Migration System EPIC-010: Configuracion Git y Repositorios
640 lines
15 KiB
TypeScript
640 lines
15 KiB
TypeScript
/**
|
|
* 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 (
|
|
* <div>
|
|
* {isAuthenticated ? (
|
|
* <p>Welcome {user.email}</p>
|
|
* ) : (
|
|
* <button onClick={() => login(credentials)}>Login</button>
|
|
* )}
|
|
* </div>
|
|
* );
|
|
* }
|
|
* ```
|
|
*
|
|
* @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<T>(
|
|
endpoint: string,
|
|
options: RequestInit = {},
|
|
): Promise<T> {
|
|
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<void> {
|
|
return this.request('/auth/logout', {
|
|
method: 'POST',
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
});
|
|
}
|
|
|
|
async refreshToken(refreshToken: string): Promise<AuthTokens> {
|
|
return this.request('/auth/refresh', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ refreshToken }),
|
|
});
|
|
}
|
|
|
|
async getProfile(accessToken: string): Promise<User> {
|
|
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<AuthState>({
|
|
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<User>) => {
|
|
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<AuthTokens | null>(storage.get());
|
|
const [lastRefresh, setLastRefresh] = useState<Date | null>(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 <DeleteButton />;
|
|
* }
|
|
*
|
|
* if (hasRole('admin')) {
|
|
* return <AdminPanel />;
|
|
* }
|
|
* ```
|
|
*/
|
|
export function usePermissions() {
|
|
const { user } = useAuth();
|
|
|
|
// Mapeo de roles a permisos (adaptar según proyecto)
|
|
const rolePermissions: Record<string, Permission[]> = {
|
|
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
|
|
* <RequirePermission resource="users" action="delete">
|
|
* <DeleteButton />
|
|
* </RequirePermission>
|
|
* ```
|
|
*/
|
|
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
|
|
* <RequireRole role="admin">
|
|
* <AdminPanel />
|
|
* </RequireRole>
|
|
*
|
|
* <RequireRole roles={['admin', 'moderator']} requireAll={false}>
|
|
* <ModeratorTools />
|
|
* </RequireRole>
|
|
* ```
|
|
*/
|
|
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<P extends object>(
|
|
Component: React.ComponentType<P>,
|
|
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 <div>Loading...</div>;
|
|
}
|
|
|
|
if (!isAuthenticated) {
|
|
return null;
|
|
}
|
|
|
|
return <Component {...props} />;
|
|
};
|
|
}
|
|
|
|
// ============ EXPORTS ============
|
|
|
|
export type {
|
|
User,
|
|
AuthTokens,
|
|
LoginCredentials,
|
|
RegisterData,
|
|
AuthState,
|
|
Permission,
|
|
};
|
|
|
|
export { authAPI, storage };
|