import type { NextFunction, Request, Response } from 'express' import { config } from '../config/index.js' import { verificarToken, type TokenPayload } from '../services/auth.service.js' // Rutas públicas: no requieren token aunque AUTH_ENABLED=true. Se comparan // contra req.path, que aquí ya está relativo al mount /api. const RUTAS_PUBLICAS: ReadonlyArray<{ metodo: string; ruta: string }> = [ { metodo: 'GET', ruta: '/health' }, { metodo: 'POST', ruta: '/auth/login' }, { metodo: 'GET', ruta: '/push/vapid-key' }, ] // Rutas públicas con parámetros (se comparan contra req.path relativo a /api). // El endpoint público de cotización es el destino del QR del PDF: el cliente // final lo abre sin sesión; devuelve solo datos comerciales, nunca del cliente. const PATRONES_PUBLICOS: ReadonlyArray<{ metodo: string; patron: RegExp }> = [ { metodo: 'GET', patron: /^\/cotizaciones\/[^/]+\/publico$/ }, // Liga remota de firma: públicos pero con token propio (scope 'firma-remota') // que se verifica dentro del controlador; no aceptan tokens de login. { metodo: 'GET', patron: /^\/clientes\/publico\/[^/]+$/ }, { metodo: 'POST', patron: /^\/clientes\/publico\/[^/]+\/firma$/ }, { metodo: 'POST', patron: /^\/clientes\/publico\/[^/]+\/documentos$/ }, // Micrositio: fichas públicas de desarrollos y su inventario disponible // (solo información comercial, jamás datos de clientes ni cotizaciones). { metodo: 'GET', patron: /^\/publico\/desarrollos$/ }, { metodo: 'GET', patron: /^\/publico\/desarrollos\/[^/]+$/ }, ] declare module 'express-serve-static-core' { interface Request { usuarioToken?: TokenPayload } } /** * Exige Bearer JWT en todo /api salvo las rutas públicas. * Con AUTH_ENABLED=false (tests) deja pasar todo. */ export function authMiddleware(req: Request, res: Response, next: NextFunction): void { if (!config.auth.enabled) { next() return } const publica = RUTAS_PUBLICAS.some((r) => r.metodo === req.method && req.path === r.ruta) || PATRONES_PUBLICOS.some((r) => r.metodo === req.method && r.patron.test(req.path)) if (publica) { next() return } // Token interno script→API: solo abre el endpoint de alertas de inventario. // Los syncs corren en este mismo host y no tienen JWT de usuario. La guarda // exige token no vacío Y header idéntico (un header ausente nunca matchea). if ( req.method === 'POST' && req.path === '/cotizaciones/alertas-inventario' && typeof config.internalSyncToken === 'string' && config.internalSyncToken.length > 0 && req.headers['x-internal-token'] === config.internalSyncToken ) { next() return } const header = req.headers.authorization const token = header?.startsWith('Bearer ') ? header.slice('Bearer '.length).trim() : null if (!token) { res.status(401).json({ error: 'Token requerido' }) return } try { req.usuarioToken = verificarToken(token) next() } catch { res.status(401).json({ error: 'Token inválido o expirado' }) } }