import { extname, join } from 'node:path'; import type { NestExpressApplication } from '@nestjs/platform-express'; import type { NextFunction, Request, Response } from 'express'; /** Nombres con hash de Angular (main-HFVHQMEH.js, styles-RS7YTGMB.css): no cambian, se pueden cachear un año. */ const CON_HASH = /-[A-Z0-9]{8}\.(js|css)$/; /** * Cabeceras de seguridad básicas. HSTS solo si la API atiende por HTTPS: con HTTP obligaría al * navegador a usar un HTTPS que no existe. */ export function cabecerasSeguridad(https: boolean) { return (_req: Request, res: Response, next: NextFunction): void => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('Referrer-Policy', 'same-origin'); if (https) res.setHeader('Strict-Transport-Security', 'max-age=31536000'); next(); }; } /** * La API sirve también la web compilada, así que todo queda en el mismo origen (sin CORS y con la * cookie de sesión SameSite=strict). Las rutas de Angular (sin extensión y fuera de /api) devuelven * index.html; un archivo que no existe responde 404 en lugar de index.html. */ export function servirWeb(app: NestExpressApplication, carpeta: string): void { app.useStaticAssets(carpeta, { index: false, setHeaders: (res, ruta) => { res.setHeader('Cache-Control', CON_HASH.test(ruta) ? 'public, max-age=31536000, immutable' : 'no-cache'); }, }); app.use((req: Request, res: Response, next: NextFunction) => { const esRutaDeLaApp = (req.method === 'GET' || req.method === 'HEAD') && !req.path.startsWith('/api') && extname(req.path) === ''; if (!esRutaDeLaApp) return next(); res.setHeader('Cache-Control', 'no-cache'); res.sendFile(join(carpeta, 'index.html'), (error) => error && next(error)); }); }