import { NextRequest, NextResponse } from "next/server"; /** * Next.js 16 Proxy (replaces deprecated Middleware) * * Handles: * 1. Optimistic auth checks - redirects unauthenticated users to login * 2. CSP nonce generation for inline script protection * 3. Security headers * * @see https://nextjs.org/docs/app/guides/authentication * @see https://nextjs.org/docs/app/guides/content-security-policy * @see https://nextjs.org/docs/messages/middleware-to-proxy */ /** * Public paths that don't require authentication. * These routes are accessible without auth cookies. */ const PUBLIC_PATHS = ["/auth/", "/", "/services", "/about", "/contact", "/vpn-configuration"]; /** * Check if a path is public (doesn't require authentication) */ function isPublicPath(pathname: string): boolean { return PUBLIC_PATHS.some(publicPath => { if (publicPath.endsWith("/")) { // Path prefix match (e.g., /auth/ matches /auth/login) return pathname.startsWith(publicPath); } // Exact match return pathname === publicPath; }); } /** * Build Content Security Policy header */ function buildCSP(nonce: string, isDev: boolean): string { if (isDev) { // Development: More permissive for HMR and dev tools return [ "default-src 'self'", "script-src 'self' 'unsafe-eval' 'unsafe-inline'", // HMR needs eval "style-src 'self' 'unsafe-inline'", "img-src 'self' data: https:", "font-src 'self' data:", "connect-src 'self' https: http://localhost:* ws://localhost:*", "frame-src 'self' https://www.google.com", "frame-ancestors 'none'", ].join("; "); } // Production: Strict CSP with nonce // 'strict-dynamic' allows scripts loaded by nonced scripts to execute. // Next 16 applies the nonce to its own inline scripts, so 'unsafe-inline' // is not required in script-src when the nonce is present. return [ "default-src 'self'", `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`, "style-src 'self' 'unsafe-inline'", // Next.js requires this for styled-jsx "img-src 'self' data: https:", "font-src 'self' data:", "connect-src 'self' https:", "frame-src 'self' https://www.google.com", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", "object-src 'none'", "upgrade-insecure-requests", ].join("; "); } export function proxy(request: NextRequest) { const { pathname } = request.nextUrl; // --- Auth Check --- // Skip auth for public routes if (!isPublicPath(pathname)) { // Check for auth cookies (optimistic check - BFF validates the token) const hasAccessToken = request.cookies.has("access_token"); const hasRefreshToken = request.cookies.has("refresh_token"); if (!hasAccessToken && !hasRefreshToken) { // No auth cookies present - redirect to login const loginUrl = new URL("/auth/login", request.url); loginUrl.searchParams.set("next", pathname); return NextResponse.redirect(loginUrl); } } // --- CSP & Security Headers --- // Generate a random nonce for this request const nonce = Buffer.from(crypto.randomUUID()).toString("base64"); const isDev = process.env["NODE_ENV"] === "development"; const cspHeader = buildCSP(nonce, isDev); // Clone the request headers and add nonce const requestHeaders = new Headers(request.headers); requestHeaders.set("x-nonce", nonce); // Create response with updated headers const response = NextResponse.next({ request: { headers: requestHeaders, }, }); // Set CSP header on response response.headers.set("Content-Security-Policy", cspHeader); // Add additional security headers response.headers.set("X-Frame-Options", "DENY"); response.headers.set("X-Content-Type-Options", "nosniff"); response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); response.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()"); return response; } // Apply proxy to all routes except static assets export const config = { matcher: [ /* * Match all request paths except: * - api (API routes) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico, sitemap.xml, robots.txt (metadata files) * - _health (health check endpoint) * - Static assets (svg, png, jpg, etc.) */ "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|_health|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|woff|woff2|ttf|eot)$).*)", ], };