Files
FRASA-CAS/api-gateway/src/middleware/auth.middleware.ts
Consult AS Technology d6548999bf feat(desarrollos): modulo de alta de desarrollos con inventario, reglas del cotizador por desarrollo y micrositio publico
Modulo nuevo (solo admin) para dar de alta desarrollos sin tocar codigo:
- CRUD de la ficha, modelos e inventario (alta manual y generador por rango)
- Reglas del cotizador por desarrollo, editables y persistidas en la BD
- El cotizador consume las reglas desde /desarrollos/reglas-cotizador y cae a
  las constantes con aviso cuando un desarrollo aun no tiene reglas guardadas
- Micrositio publico en /d/:clave (sin sesion) con ficha, modelos y
  disponibilidad; jamas expone datos de clientes ni cotizaciones

Integracion:
- La landing monta el modulo en el grid (clave 'desarrollos', solo admin) y el
  micrositio fuera del login; el rol manager NO accede (matriz y API con 403)
- La tabla de roles de Configuracion ahora lista todos los modulos
- Se alinea la FK de ReglaCotizadorDesarrollo con la BD viva (Desarrollo.id)

Verificado: 708 pruebas del API, 115 del cotizador y 89 de la landing en verde;
12/12 cotizaciones identicas antes y despues del cambio de reglas; prueba en
vivo del API con alta, modelos, inventario, reglas, micrositio y baja (30/30).
2026-09-21 09:52:47 +00:00

81 lines
3.0 KiB
TypeScript

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' })
}
}