Frontend Authentication
Frontend Authentication
Version: 1.0 Date: 2025-11-19 Project: Algesta Dashboard React
Tabla de Contenidos
- Descripción General de Autenticación
- Arquitectura de Autenticación
- Configuración de Autenticación
- Integración con Cliente API
- Componentes de Autenticación
- Esquemas de Validación
- Autorización (RBAC)
- Servicio de Almacenamiento
- Flujo de Estado de Autenticación
- Consideraciones de Seguridad
- Mejores Prácticas
- Pruebas de Autenticación
Descripción General de Autenticación
El Dashboard de Algesta implementa un sistema de autenticación seguro con las siguientes funcionalidades:
- Autenticación basada en JWT con almacenamiento de tokens
- localStorage para persistencia de tokens
- Integración con react-query-auth para gestión de estado de autenticación
- Cierre de sesión automático al expirar el token (respuestas 401)
- Control de acceso basado en roles (RBAC) para autorización
Arquitectura de Autenticación
sequenceDiagram
participant User
participant LoginForm
participant AuthLib
participant APIClient
participant APIGateway
participant AuthMS
participant LocalStorage
User->>LoginForm: Enter credentials
LoginForm->>AuthLib: useLogin(credentials)
AuthLib->>APIClient: POST /api/auth/login
APIClient->>APIGateway: Forward request
APIGateway->>AuthMS: Authenticate user
AuthMS-->>APIGateway: Return token + user
APIGateway-->>APIClient: Return response
APIClient-->>AuthLib: Return data
AuthLib->>LocalStorage: Store token
AuthLib->>LocalStorage: Store user
AuthLib-->>LoginForm: Return user
LoginForm->>User: Redirect to dashboard
Configuración de Autenticación
La configuración se define en src/lib/auth.tsx.
Configuración de Auth
import { configureAuth } from 'react-query-auth';import { getUser, loginWithEmailAndPassword, registerWithEmailAndPassword, logout } from './api';
const authConfig = { userFn: getUser, // Get current user loginFn: loginWithEmailAndPassword, // Login function registerFn: registerWithEmailAndPassword, // Register function logoutFn: logout, // Logout function};
export const { useUser, useLogin, useLogout, useRegister, AuthLoader } = configureAuth(authConfig);Recuperación de Usuario
const getUser = async (): Promise<User | null> => { try { const userObj = getUserObj(); // Get from localStorage if (userObj?.id) return userObj; return null; } catch (error) { console.error('Error fetching user:', error); return null; }};
export const getUserObj = (): User | null => { const user = storageService.get<User | null>(StorageKeys.User); if (!user?.id) return null; return user;};Propósito: Recuperar usuario autenticado desde localStorage al inicializar la aplicación.
Gestión de Tokens
export const getToken = () => { const token = storageService.get(StorageKeys.Token); if (!token) return null; return token;};Propósito: Recuperar token JWT para solicitudes API.
Flujo de Login
const loginWithEmailAndPassword = (data: LoginFormSchemaProps): Promise<AuthResponse> => { return apiClient.post('/api/auth/login', data);};
const loginFn = async (data: LoginFormSchemaProps) => { const response = await loginWithEmailAndPassword(data); const responseData = response.data.data; const token = responseData?.token; const user = responseData?.user;
if (user && token) { storageService.set(StorageKeys.Token, token); storageService.set(StorageKeys.User, user); return user; }
return null;};Pasos:
- Enviar credenciales a
/api/auth/login - Recibir token y datos de usuario
- Almacenar token en localStorage
- Almacenar datos de usuario en localStorage
- Retornar usuario para gestión de estado
Flujo de Logout
export const logout = (): Promise<void> => { return new Promise(resolve => { removeAuthStorageKeys(); resolve(); });};
export const removeAuthStorageKeys = () => { storageService.remove(StorageKeys.Token); storageService.remove(StorageKeys.User);};Pasos:
- Eliminar token de localStorage
- Eliminar usuario de localStorage
- Redirigir a página de login (manejado por enrutamiento)
Flujo de Registro
const registerWithEmailAndPassword = (data: RegisterInput): Promise<AuthResponse> => { return apiClient.post('/api/auth/register', data);};
const registerFn = async (data: RegisterInput) => { const response = await registerWithEmailAndPassword(data); return response.data.data.user;};Pasos:
- Enviar datos de registro a
/api/auth/register - Recibir datos de usuario (el token puede estar incluido)
- Retornar usuario para gestión de estado
- Opcionalmente auto-login después del registro
Integración con Cliente API
Interceptor de Solicitud (Agregar Token JWT)
Ubicado en src/lib/api-client.ts:
const authRequestInterceptor = async (config: InternalAxiosRequestConfig) => { const token = getToken();
if (token) { config.headers.Authorization = `Bearer ${token}`; }
return config;};
API_CLIENT.interceptors.request.use(authRequestInterceptor);Propósito: Agregar automáticamente el token JWT a todas las solicitudes API.
Interceptor de Respuesta (Manejar Errores 401)
const authResponseInterceptor = async (error: AxiosError) => { if (error.response?.status === 401 && error.config?.url !== '/api/auth/login') { // Token expired or invalid SessionManager.clearSession(); toast.error('La sesión ha caducado, ingresa nuevamente'); return Promise.reject(new Error(error.message)); }
// Handle other errors const message = error.response?.data?.message || 'An error occurred'; toast.error(message);
return Promise.reject(new Error(error.message));};
API_CLIENT.interceptors.response.use( response => response, authResponseInterceptor);Propósito: Cerrar sesión automáticamente y redirigir en respuestas 401 (No autorizado).
Gestor de Sesión
export const SessionManager: SessionManagerProps = { isRefreshing: false,
clearSession: () => { // Remove all auth-related data storageService.remove(StorageKeys.RefreshToken); storageService.remove(StorageKeys.Token); storageService.remove(StorageKeys.User); storageService.remove(StorageKeys.DetailUserClient); storageService.remove(StorageKeys.DetailUserProvider); storageService.remove(StorageKeys.QuotationSelectProvider);
// Redirect to login setTimeout(() => { window.location.href = paths.auth.login.getHref(); }, 300); },};Propósito: Limpiar todos los datos de sesión y redirigir al login al cerrar sesión o expirar la sesión.
Componentes de Autenticación
1. Formulario de Login
Ubicado en src/Funcionalidades/auth/Componentes/login-form.tsx:
Funcionalidades:
- Campos de entrada para email y contraseña
- Validación de formulario con Zod
- Manejador de envío usando hook
useLogin - Manejo y visualización de errores
- Enlace a recuperación de contraseña
- Enlaces a registro
Ejemplo:
import { useLogin } from '@/lib/auth';import { useForm } from 'react-hook-form';import { zodResolver } from '@hookform/resolvers/zod';import { loginSchema } from '../validations/login-schema';
const LoginForm = () => { const { mutate: login, isPending } = useLogin();
const form = useForm({ resolver: zodResolver(loginSchema), defaultValues: { email: '', password: '', }, });
const onSubmit = form.handleSubmit((data) => { login(data); });
return ( <Form {...form}> <form onSubmit={onSubmit}> <InputTextFormField name="email" control={form.control} label="Email" type="email" /> <InputTextFormField name="password" control={form.control} label="Password" type="password" /> <Button type="submit" disabled={isPending}> {isPending ? 'Logging in...' : 'Login'} </Button> </form> </Form> );};2. Signup Forms
Client Signup (signup-client-form.tsx):
- Client-specific registration form
- Additional fields: company name, address, etc.
Provider Signup (signup-provider-form.tsx):
- Provider-specific registration form
- Additional fields: services offered, certifications, etc.
Base Form (signup-base-form.tsx):
- Shared form logic and validation
- Common fields: email, password, name, etc.
3. Password Recovery
Forgot Password (forgot-password-form.tsx):
- Email input to request password reset
- Sends reset link to email
Reset Password (reset-password-form.tsx):
- New password input
- Confirm password validation
- Token validation from URL
4. Auth Layout
Located in src/Funcionalidades/auth/Componentes/auth-layout.tsx:
Funcionalidades:
- Consistent layout for all auth pages
- Branding and logo
- Responsive design
- Background styling
5. Auth Loader
Propósito: Display loading state while checking authentication Estado.
Validation Schemas
Login Schema
Located in src/Funcionalidades/auth/validations/login-schema.ts:
import { z } from 'zod';
export const loginSchema = z.object({ email: z.string().email('Email inválido'), password: z.string().min(6, 'La contraseña debe tener al menos 6 caracteres'),});
export type LoginFormSchemaProps = z.infer<typeof loginSchema>;Registration Schema
Located in src/utils/schemas/auth.ts:
import { z } from 'zod';
export const registerSchema = z.object({ email: z.string().email('Email inválido'), password: z.string().min(6, 'La contraseña debe tener al menos 6 caracteres'), confirmPassword: z.string(), name: z.string().min(2, 'El nombre debe tener al menos 2 caracteres'), lastName: z.string().min(2, 'El apellido debe tener al menos 2 caracteres'), phone: z.string().optional(), // ... more fields}).refine((data) => data.password === data.confirmPassword, { message: 'Las contraseñas no coinciden', path: ['confirmPassword'],});
export type RegisterInput = z.infer<typeof registerSchema>;Password Reset Schema
export const resetPasswordSchema = z.object({ password: z.string().min(6, 'La contraseña debe tener al menos 6 caracteres'), confirmPassword: z.string(),}).refine((data) => data.password === data.confirmPassword, { message: 'Las contraseñas no coinciden', path: ['confirmPassword'],});Authorization (RBAC)
User Roles
Defined in src/types/authorization.ts:
export const ROLES = { ADMIN: 'admin', AGENT: 'agent', PROVIDER: 'provider', CLIENT: 'client',} as const;
export type RoleTypes = typeof ROLES[keyof typeof ROLES];User Type
export interface User { id: string; email: string; avatar?: string; role?: RoleTypes; name: string; lastName: string; phone?: string; // ... more fields}Authorization Hook
Located in src/lib/authorization.tsx:
export const useAuthorization = () => { const user = useUser();
if (!user.data) { throw Error('User does not exist!'); }
const checkAccess = React.useCallback( ({ allowedRoles }: { allowedRoles: RoleTypes[] }) => { if (allowedRoles && allowedRoles.length > 0 && user.data) { return allowedRoles?.includes(user.data.role!); } return true; }, [user.data] );
return { checkAccess, role: user.data.role };};Authorization Componente
export const Authorization = ({ policyCheck, allowedRoles, forbiddenFallback = null, children}: AuthorizationProps) => { const { checkAccess } = useAuthorization();
let canAccess = false;
if (allowedRoles) { canAccess = checkAccess({ allowedRoles }); }
if (typeof policyCheck !== 'undefined') { canAccess = policyCheck; }
return <>{canAccess ? children : forbiddenFallback}</>;};Usage Example:
import { Authorization, ROLES } from '@/lib/authorization';
const AdminPanel = () => { return ( <Authorization allowedRoles={[ROLES.ADMIN, ROLES.AGENT]}> <AdminOnlyContent /> </Authorization> );};Route-Level Authorization
See Frontend Routing & Navigation for route protection details.
Policies
Defined in src/lib/authorization.tsx:
export const POLICIES = { 'comment:delete': (user: User) => { if (user.role === ROLES.ADMIN || user.role === ROLES.AGENT) { return true; } return false; },
// ... more policies};Note: The examples above ('order:delete', 'user:create', etc.) are representative policy patterns. For the Completo and up-to-date list of policies actually implemented in the system, refer to src/lib/authorization.tsx. The current Implementación includes policies like 'comment:delete' and route-based policies in the ROUTE_POLICIES object.
Usage:
const { checkAccess } = useAuthorization();const user = useUser();
const canDeleteOrder = POLICIES['order:delete'](user.data);
<Authorization policyCheck={canDeleteOrder}> <DeleteOrderButton /></Authorization>Storage Service
Located in src/lib/storage.ts:
Propósito: Abstraction over localStorage with type safety.
Métodos
export const storageService = { get<T>(key: string): T | null { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : null; } catch { return null; } },
set<T>(key: string, value: T): void { try { localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.error('Error saving to localStorage:', error); } },
remove(key: string): void { try { localStorage.removeItem(key); } catch (error) { console.error('Error removing from localStorage:', error); } },
clear(): void { try { localStorage.clear(); } catch (error) { console.error('Error clearing localStorage:', error); } },};Storage Keys
Defined in src/config/constants.ts (authoritative source):
export const StorageKeys = { Token: 'auth_token', RefreshToken: 'refresh_token', User: 'U534', DetailUserClient: 'detail_client', DetailUserProvider: 'detail_provider', QuotationSelectProvider: 'quotation_select_provider',} as const;Note: These are the actual localStorage keys used by the application. The key names and Valors must match exactly what is defined in src/config/constants.ts to ensure proper storage and retrieval of authentication data.
Authentication State Flow
stateDiagram-v2
[*] --> Unauthenticated
Unauthenticated --> Authenticating: Login attempt
Authenticating --> Authenticated: Success
Authenticating --> Unauthenticated: Failure
Authenticated --> Unauthenticated: Logout
Authenticated --> Unauthenticated: Token expired (401)
Authenticated --> Authenticated: Token valid
Security Considerations
1. Token Storage
Current Implementación:
- JWT stored in localStorage (not cookies)
- Vulnerable to XSS attacks if script injection occurs
- Mitigated by Content Security Policy
Production Recommendations (not yet implemented):
The following items are recommendations for future enhancements and are not currently implemented in the codebase:
- Consider httpOnly cookies for token storage
- Implement refresh token rotation
- Use secure and sameSite cookie flags
Note: Migration to httpOnly cookies would require coordinated changes on both frontend and backend (API Gateway and Auth Microservicio).
2. Token Expiration
- Tokens expire after configured time (set by backend)
- 401 responses trigger automatic logout
- User redirected to login page
- All auth data cleared from storage
3. HTTPS
- Always use HTTPS in production
- Prevents token interception (man-in-the-middle attacks)
- SSL/TLS encryption for data in transit
4. CORS
- API Gateway configured with CORS
- Only allow trusted origins
- Credentials included in requests
5. Password Security
Client-Side:
- Passwords validated with Zod (minimum length, complexity)
- Not stored in application state
- Sent securely over HTTPS
Server-Side (Backend):
- Passwords hashed with bcrypt
- Salt rounds configured appropriately
- Never stored in plain text
6. XSS Protection
- React automatically escapes output
- Content Security Policy headers
- Sanitize user input
7. CSRF Protection
- Stateless JWT authentication (no session cookies)
- CSRF tokens not required for JWT-based auth
- Use httpOnly cookies for additional CSRF protection
Best Practices
1. Token Management
- Store token securely (consider httpOnly cookies)
- Clear token on logout Completoly
- Handle token expiration gracefully (automatic logout)
- Consider refresh token Implementación for better UX
2. User Experience
- Show loading states during authentication
- Display clear error messages (invalid credentials, server errors)
- Redirect to intended route after login (remember previous location)
- Remember user preferences (theme, language)
3. Error Handling
- Handle network errors (offline, timeout)
- Handle invalid credentials (wrong email/password)
- Handle token expiration (401 responses)
- Provide user feedback (toast notifications, inline errors)
4. Authorization
- Check roles on both client and server (never trust client)
- Use Authorization Componente for UI-level control
- Use route policies for navigation control
- Implement fine-grained permissions with policies
5. Validation
- Validate all inputs with Zod schemas
- Provide clear validation messages
- Validate on both client and server
- Handle validation errors gracefully
6. Pruebas
- Test authentication flows (login, logout, registration)
- Test authorization checks (role-based access)
- Test token expiration (401 handling)
- Test error scenarios (network errors, invalid credentials)
Pruebas Authentication
Unit Pruebas
Example in src/Pruebas/Pruebas/login-form.test.tsx:
import { render, screen, fireEvent, waitFor } from '@testing-library/react';import { LoginForm } from '@/features/auth/components/login-form';
describe('LoginForm', () => { it('should render login form', () => { render(<LoginForm />); expect(screen.getByLabelText('Email')).toBeInTheDocument(); expect(screen.getByLabelText('Password')).toBeInTheDocument(); });
it('should show validation errors for invalid inputs', async () => { render(<LoginForm />); fireEvent.click(screen.getByRole('button', { name: /login/i }));
await waitFor(() => { expect(screen.getByText('Email inválido')).toBeInTheDocument(); }); });
it('should submit form with valid credentials', async () => { const mockLogin = jest.fn(); render(<LoginForm />);
fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'test@example.com' }, }); fireEvent.change(screen.getByLabelText('Password'), { target: { value: 'password123' }, }); fireEvent.click(screen.getByRole('button', { name: /login/i }));
await waitFor(() => { expect(mockLogin).toHaveBeenCalledWith({ email: 'test@example.com', password: 'password123', }); }); });});Integration Pruebas
Test Completo authentication flow:
describe('Authentication Flow', () => { it('should login and redirect to dashboard', async () => { // Test login → store token → redirect });
it('should logout and redirect to login', async () => { // Test logout → clear token → redirect });
it('should handle 401 and auto-logout', async () => { // Test 401 response → clear session → redirect });});E2E Pruebas (Playwright)
test('login flow', async ({ page }) => { await page.goto('/auth/login'); await page.fill('input[name="email"]', 'test@example.com'); await page.fill('input[name="password"]', 'password123'); await page.click('button[type="submit"]'); await page.waitForURL('/app/dashboard'); expect(page.url()).toContain('/app/dashboard');});References:
algesta-dashboard-react/src/lib/auth.tsxalgesta-dashboard-react/src/lib/authorization.tsxalgesta-dashboard-react/src/lib/api-client.tsalgesta-dashboard-react/src/lib/storage.tsalgesta-dashboard-react/src/Funcionalidades/auth/algesta-dashboard-react/src/types/authorization.ts