Files
property-management-network/proxy.ts
T

107 lines
3.6 KiB
TypeScript
Raw Normal View History

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"]
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 {
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"
2026-07-01 13:56:34 -04:00
return [
"default-src 'self'",
"img-src 'self' data: blob: https:",
"style-src 'self' 'unsafe-inline'",
scriptSrc,
2026-07-01 13:56:34 -04:00
"font-src 'self' data:",
connectSrc,
2026-07-01 13:56:34 -04:00
"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)
}
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
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
}