Middleware
La API de Zentto utiliza una cadena de middleware Express para manejar autenticación,
conversión de fechas, errores, CORS y logging. Cada middleware tiene una responsabilidad
única y se registra en orden específico en app.ts.
Orden de ejecución
Request
│
▼
┌─────────────┐
│ CORS │ Valida origen, headers permitidos
└──────┬──────┘
▼
┌─────────────┐
│ JSON body │ express.json() — parsea body
└──────┬──────┘
▼
┌─────────────┐
│ Datetime │ Convierte fechas del request a UTC
└──────┬──────┘
▼
┌─────────────┐
│ Auth │ Verifica JWT (en rutas protegidas)
└──────┬──────┘
▼
┌─────────────┐
│ Router │ Ejecuta la lógica del endpoint
└──────┬──────┘
▼
┌─────────────┐
│ Error │ Captura errores y responde formato estándar
└─────────────┘
▼
Response
Auth middleware — Verificación JWT
Ubicado en web/api/src/middleware/auth.ts. Verifica el token JWT enviado
en el header Authorization: Bearer <token> y adjunta los datos del
usuario al objeto req.user.
// web/api/src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
interface JwtPayload {
userId: number;
companyId: number;
email: string;
roles: string[];
}
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
const header = req.headers.authorization;
if (!header || !header.startsWith('Bearer ')) {
return res.status(401).json({ ok: false, error: 'Token requerido' });
}
const token = header.slice(7);
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
req.user = {
userId: payload.userId,
companyId: payload.companyId,
email: payload.email,
roles: payload.roles
};
next();
} catch (err) {
return res.status(401).json({ ok: false, error: 'Token inválido o expirado' });
}
}
La verificación de contraseñas usa bcrypt en Node.js, nunca en SQL.
El SP usp_Sec_User_ValidateLogin retorna el hash almacenado y la API
compara con bcrypt.compare().
Datetime middleware — Conversión UTC
Ubicado en web/api/src/middleware/datetime.ts. Implementa la regla
"UTC-0 a fuego": convierte todas las fechas entrantes a UTC y las
salientes al timezone de la empresa.
// web/api/src/middleware/datetime.ts
import { Request, Response, NextFunction } from 'express';
export function datetimeMiddleware(req: Request, res: Response, next: NextFunction) {
// 1. Convertir fechas del body a UTC (si vienen con timezone)
if (req.body && typeof req.body === 'object') {
convertDatesToUtc(req.body);
}
// 2. Interceptar res.json para convertir fechas de respuesta
const originalJson = res.json.bind(res);
res.json = (data: any) => {
const timezone = req.headers['x-timezone'] || 'America/Caracas';
const converted = convertDatesFromUtc(data, timezone as string);
return originalJson(converted);
};
next();
}
function convertDatesToUtc(obj: any): void {
for (const key of Object.keys(obj)) {
if (typeof obj[key] === 'string' && isDateString(obj[key])) {
obj[key] = new Date(obj[key]).toISOString();
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
convertDatesToUtc(obj[key]);
}
}
}
function convertDatesFromUtc(obj: any, timezone: string): any {
// Recorre recursivamente y convierte campos *Utc a timezone local
// ...
}
Header X-Timezone
El frontend envía el header X-Timezone con el timezone del usuario
(ej. America/Caracas, Europe/Madrid). La API usa este
valor para convertir las fechas de respuesta al horario local. Si no se envía,
se usa America/Caracas por defecto.
Error handler — Manejo global de errores
Ubicado en web/api/src/middleware/errorHandler.ts. Captura cualquier error
no manejado y retorna una respuesta estándar:
// web/api/src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
export function errorHandler(err: any, req: Request, res: Response, next: NextFunction) {
console.error(`[ERROR] ${req.method} ${req.path}:`, err.message);
// Errores conocidos con statusCode
if (err.statusCode) {
return res.status(err.statusCode).json({
ok: false,
error: err.message,
statusCode: err.statusCode
});
}
// Error de validación
if (err.name === 'ValidationError') {
return res.status(400).json({
ok: false,
error: err.message,
statusCode: 400
});
}
// Error de BD
if (err.code === 'EREQUEST' || err.code === '23505') {
return res.status(409).json({
ok: false,
error: 'Conflicto de datos',
statusCode: 409
});
}
// Error genérico (no exponer detalles internos)
return res.status(500).json({
ok: false,
error: 'Error interno del servidor',
statusCode: 500
});
}
CORS — Configuración de orígenes
La configuración CORS permite solicitudes desde los dominios autorizados. En producción,
la mayor parte del manejo CORS ocurre a nivel de Nginx con el flag
always para que funcione incluso en respuestas de error.
// web/api/src/middleware/cors.ts (Express level)
import cors from 'cors';
const allowedOrigins = [
'https://zentto.net',
'https://www.zentto.net',
'http://localhost:3000', // Desarrollo local
];
export const corsConfig = cors({
origin: (origin, callback) => {
// Permitir requests sin origin (mobile apps, Postman)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
callback(new Error('CORS no permitido'));
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Timezone']
});
# nginx/zentto.conf (Nginx level — producción)
location /api/ {
proxy_pass http://localhost:4000/;
# CORS headers (always = incluso en 4xx/5xx)
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Timezone" always;
add_header Access-Control-Allow-Credentials "true" always;
if ($request_method = OPTIONS) {
return 204;
}
}
Request logging
En desarrollo se usa morgan para logging de solicitudes HTTP.
En producción, los logs se escriben en stdout y Docker los captura:
// En app.ts
import morgan from 'morgan';
if (process.env.NODE_ENV !== 'production') {
app.use(morgan('dev'));
// Salida: GET /v1/inventario/productos 200 12ms
} else {
app.use(morgan('combined'));
// Salida: ::ffff:172.18.0.1 - - [22/Mar/2026:...] "GET /v1/inventario/productos" 200 1423
}
Resumen de archivos
| Archivo | Responsabilidad |
|---|---|
| middleware/auth.ts | Verificar JWT, adjuntar user al request |
| middleware/datetime.ts | Convertir fechas request a UTC, response a timezone local |
| middleware/errorHandler.ts | Capturar errores, responder formato estándar |
| middleware/cors.ts | Validar orígenes, headers, métodos permitidos |