import { NextRequest, NextResponse } from 'next/server'; // Routes that should NOT redirect to home on hard refresh const PRESERVED_ROUTES = [ '/booking/', '/login', '/register', '/forgot-password', '/reset-password', '/set-password', '/verify-account', '/fayda-setup', '/profile', '/about', '/contact', '/help', '/guide', '/services', '/packages', '/go/', '/packages/', ]; /** * Build the Content-Security-Policy for a single request. * * Production uses a strict, nonce-based policy with `strict-dynamic`: only scripts * carrying this request's nonce (and scripts they load) may execute, which neutralises * reflected/stored XSS regardless of any host allowlist. Next.js applies the nonce to * its own bootstrap/chunk scripts automatically because middleware forwards it on the * request `Content-Security-Policy` header (see below); our own inline scripts read it * from the `x-nonce` request header in the root layout. * * Development relaxes `script-src` (Next.js HMR/react-refresh needs `unsafe-eval` and * inline) and allows the HMR websocket, and drops `upgrade-insecure-requests` so * plain-HTTP localhost keeps working. */ function buildCsp(nonce: string): string { const isProd = process.env.NODE_ENV === 'production'; // Origin the browser calls for API/XHR/fetch — must be allowed in connect-src. let apiOrigin = ''; try { apiOrigin = new URL(process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000').origin; } catch { apiOrigin = ''; } const scriptSrc = isProd ? `'self' 'nonce-${nonce}' 'strict-dynamic'` : `'self' 'unsafe-inline' 'unsafe-eval'`; // The Socket.IO WebSocket upgrade connects to wss://; under CSP a // `https://host` source does NOT cover `wss://host`, so add it explicitly. const wsOrigin = apiOrigin.replace(/^http/, 'ws'); // https→wss, http→ws const connectSrc = isProd ? `'self' ${apiOrigin} ${wsOrigin}`.trim() : `'self' ${apiOrigin} ws: wss:`.trim(); const directives = [ `default-src 'self'`, `base-uri 'self'`, `script-src ${scriptSrc}`, // Inline styles (Tailwind runtime + React `style=` attributes) can't execute JS; // nonce-ing them reliably breaks Next/React, so 'unsafe-inline' is the accepted stance. `style-src 'self' 'unsafe-inline'`, `img-src 'self' data: blob: ${apiOrigin}`.trim(), `font-src 'self' data:`, `connect-src ${connectSrc}`, `worker-src 'self' blob:`, `frame-src 'self'`, `object-src 'none'`, `form-action 'self'`, `frame-ancestors 'none'`, ...(isProd ? ['upgrade-insecure-requests'] : []), ]; return directives.join('; '); } export function middleware(request: NextRequest) { const nonce = btoa(crypto.randomUUID()); const csp = buildCsp(nonce); // Forward the nonce + CSP on the *request* so Next.js nonces its own scripts and our // layout can read `x-nonce`. The browser-enforced copy is set on the response below. const requestHeaders = new Headers(request.headers); requestHeaders.set('x-nonce', nonce); requestHeaders.set('Content-Security-Policy', csp); const render = () => NextResponse.next({ request: { headers: requestHeaders } }); const { pathname } = request.nextUrl; let response: NextResponse; if ( pathname.startsWith('/_next') || pathname.startsWith('/api') || pathname.includes('.') || pathname === '/' ) { // Next.js internals, static files, API routes, and home. response = render(); } else if (PRESERVED_ROUTES.some((r) => pathname.startsWith(r))) { // Public and auth routes: let them through. response = render(); } else { response = render(); } response.headers.set('Content-Security-Policy', csp); // Anti-clickjacking. `frame-ancestors 'none'` (in the CSP above) is the modern control; // X-Frame-Options: DENY is the legacy equivalent for older browsers and scanners. response.headers.set('X-Frame-Options', 'DENY'); // Tell crawlers not to index private/transactional routes. const NOINDEX_PREFIXES = [ '/booking/', '/login', '/register', '/forgot-password', '/reset-password', '/set-password', '/verify-account', '/fayda-setup', '/profile', '/go/', ]; if (NOINDEX_PREFIXES.some((p) => pathname.startsWith(p))) { response.headers.set('X-Robots-Tag', 'noindex, nofollow'); } return response; } export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], };