import { auth } from "@/src/lib/auth"; import { NextResponse } from "next/server"; import crypto from "node:crypto"; /** * Next.js Proxy for route protection. * Provides defense-in-depth by checking authentication at the edge * before requests reach page components. * * Note: Proxy always runs on Node.js runtime. */ const isDev = process.env.NODE_ENV === "development"; /** * M6: Build a nonce-based Content-Security-Policy per request. * Next.js reads the nonce from the CSP request header and applies it * to all inline scripts it generates. */ function buildCsp(nonce: string): string { const directives = [ "default-src 'self'", isDev ? `script-src 'self' 'nonce-${nonce}' 'unsafe-eval'` : `script-src 'self' 'nonce-${nonce}'`, // style-src still needs 'unsafe-inline' for React JSX inline style props "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", "font-src 'self' https://fonts.gstatic.com", "img-src 'self' data: blob:", "worker-src blob:", "connect-src 'self'", "frame-ancestors 'none'", ]; return directives.join("; "); } export default auth((req) => { const isAuthenticated = !!req.auth; const pathname = req.nextUrl.pathname; // Allow public routes if ( pathname === "/login" || pathname.startsWith("/api/auth") || pathname === "/api/health" || pathname === "/api/instances/sync" || pathname.startsWith("/api/v1/") ) { return NextResponse.next(); } // Redirect unauthenticated users to login if (!isAuthenticated && !pathname.startsWith("/login")) { const loginUrl = new URL("/login", req.url); return NextResponse.redirect(loginUrl); } // Generate per-request nonce for CSP const nonce = crypto.randomBytes(16).toString("base64"); const csp = buildCsp(nonce); // Set CSP as a request header so Next.js can read the nonce const requestHeaders = new Headers(req.headers); requestHeaders.set("Content-Security-Policy", csp); const response = NextResponse.next({ request: { headers: requestHeaders }, }); // Also set CSP as a response header for browser enforcement response.headers.set("Content-Security-Policy", csp); response.headers.set("X-Content-Type-Options", "nosniff"); response.headers.set("X-Frame-Options", "DENY"); response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); response.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), interest-cohort=()"); return response; }); export const config = { matcher: [ /* * Match all request paths except for the ones starting with: * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) * - public folder */ "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", ], };