- Borradores por vendedor (guardar/continuar/eliminar), se limpia al registrar - Pre-llenado con ?cotizacion=<id> (cliente, contacto, proyecto, montos); botón 'Iniciar alta' en Mis cotizaciones del cotizador - Liga remota pública /client/firma/<token> (JWT scope firma-remota, 72h, rate limit, solo datos mínimos sin PLD): firma digital + subida de documentos que prende checkboxes en Monday Apartado - Tokens de liga no sirven como sesión y viceversa - Tests: api 228, alta 82, cotizador 82
65 lines
2.3 KiB
TypeScript
65 lines
2.3 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.
|
|
// Monday no puede mandar JWT, así que su webhook queda abierto (se valida por
|
|
// firma/challenge dentro del controlador).
|
|
const RUTAS_PUBLICAS: ReadonlyArray<{ metodo: string; ruta: string }> = [
|
|
{ metodo: 'GET', ruta: '/health' },
|
|
{ metodo: 'POST', ruta: '/auth/login' },
|
|
{ metodo: 'POST', ruta: '/webhooks/monday' },
|
|
]
|
|
|
|
// 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$/ },
|
|
]
|
|
|
|
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
|
|
}
|
|
|
|
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' })
|
|
}
|
|
}
|