2026-06-23 20:36:07 -04:00
|
|
|
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",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const AUTH_PATHS = ["/login", "/signup", "/forgot-password"]
|
|
|
|
|
|
2026-07-01 13:56:34 -04:00
|
|
|
// 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 {
|
|
|
|
|
return [
|
|
|
|
|
"default-src 'self'",
|
|
|
|
|
"img-src 'self' data: blob: https:",
|
|
|
|
|
"style-src 'self' 'unsafe-inline'",
|
|
|
|
|
`script-src 'self' 'nonce-${nonce}' https://challenges.cloudflare.com`,
|
|
|
|
|
"font-src 'self' data:",
|
|
|
|
|
"connect-src 'self' https://api.stripe.com https://api.openai.com https://api.resend.com https://challenges.cloudflare.com",
|
|
|
|
|
"frame-src https://js.stripe.com https://hooks.stripe.com https://challenges.cloudflare.com",
|
|
|
|
|
"frame-ancestors 'none'",
|
|
|
|
|
"base-uri 'self'",
|
|
|
|
|
"form-action 'self'",
|
|
|
|
|
].join("; ")
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 20:36:07 -04:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 13:56:34 -04:00
|
|
|
// 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
|
2026-06-23 20:36:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const config = {
|
|
|
|
|
matcher: [
|
|
|
|
|
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
|
|
|
|
|
],
|
|
|
|
|
}
|