import { NextResponse, type NextRequest } from "next/server" import { getSessionCookie } from "better-auth/cookies" const PROTECTED_PATHS = [ "/admin", "/dashboard", "/properties", "/tenants", "/rent", "/maintenance", "/leases", "/expenses", "/settings", "/onboarding", "/calendar", "/inspections", "/vendors", "/reports", "/activity", "/ai", "/ai-dashboard", "/predictions", "/recommendations", "/impact", "/follow-ups", "/team", ] const AUTH_PATHS = ["/login", "/signup", "/forgot-password"] // Build the per-request Content-Security-Policy. `script-src` carries a // per-request nonce instead of 'unsafe-inline'. `style-src` keeps // 'unsafe-inline' because Radix / Tailwind / framer-motion inject inline // styles and removing it would break the UI. Next.js reads the nonce from the // `content-security-policy` request header and applies it to its own inline // scripts automatically. function buildCsp(nonce: string): string { const isDev = process.env.NODE_ENV !== "production" // In development, Next.js/React and Turbopack HMR require eval() for hot // reloading and debugging features, and open a dev websocket. These are NOT // added in production, where the nonce-based policy stays strict. const scriptSrc = isDev ? `script-src 'self' 'nonce-${nonce}' 'unsafe-eval' https://challenges.cloudflare.com` : `script-src 'self' 'nonce-${nonce}' https://challenges.cloudflare.com` const connectSrc = isDev ? "connect-src 'self' ws: wss: https://api.stripe.com https://api.openai.com https://challenges.cloudflare.com" : "connect-src 'self' https://api.stripe.com https://api.openai.com https://challenges.cloudflare.com" return [ "default-src 'self'", "img-src 'self' data: blob: https:", "style-src 'self' 'unsafe-inline'", scriptSrc, "font-src 'self' data:", connectSrc, "frame-src https://js.stripe.com https://hooks.stripe.com https://challenges.cloudflare.com", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", ].join("; ") } export async function proxy(request: NextRequest) { const pathname = request.nextUrl.pathname // Optimistic check based on the presence of the session cookie. Real // enforcement happens in routes / server components via getSessionUser(). const sessionCookie = getSessionCookie(request) const isProtected = PROTECTED_PATHS.some((p) => pathname.startsWith(p)) if (isProtected && !sessionCookie) { const url = request.nextUrl.clone() url.pathname = "/login" return NextResponse.redirect(url) } const isAuthPage = AUTH_PATHS.some((p) => pathname.startsWith(p)) if (isAuthPage && sessionCookie) { const url = request.nextUrl.clone() url.pathname = "/dashboard" return NextResponse.redirect(url) } // Per-request CSP nonce. UUID contains only hex + dashes, so it never // includes HTML-escape characters (which Next rejects in nonces). const nonce = crypto.randomUUID() const csp = buildCsp(nonce) // Forward the nonce + CSP on the request headers so Next.js can pick up the // nonce and apply it to its own inline scripts during render. const requestHeaders = new Headers(request.headers) requestHeaders.set("x-nonce", nonce) requestHeaders.set("Content-Security-Policy", csp) const response = NextResponse.next({ request: { headers: requestHeaders } }) // Also set the CSP on the outgoing response so the browser enforces it. response.headers.set("Content-Security-Policy", csp) return response } export const config = { matcher: [ "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", ], }