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).
81 lines
3.0 KiB
TypeScript
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' })
|
|
}
|
|
}
|