Files
podcastdistributiona/middleware.ts
T

89 lines
3.3 KiB
TypeScript
Raw Normal View History

import { NextRequest, NextResponse } from "next/server";
// Better Auth's session cookie name (default prefix "better-auth"); the
// "__Secure-" variant is used when cookies are served over HTTPS in production.
const SESSION_COOKIES = ["better-auth.session_token", "__Secure-better-auth.session_token"];
2026-06-20 20:59:03 -04:00
// Authed surfaces that require an optimistic session-cookie check. Anonymous users
// hitting these are redirected to /sign-in. Public/marketing/auth routes are NOT
// listed here, so they are never redirected (CSP still applies to them, below).
const AUTHED_PREFIXES = [
"/dashboard",
"/episodes",
"/series",
"/usage",
"/billing",
"/team",
"/api-keys",
"/settings",
"/admin",
];
/**
2026-06-20 20:59:03 -04:00
* Runs on every request (see matcher). Two responsibilities:
*
* 1. CSP/nonce (all routes): generate a per-request base64 nonce with the Web Crypto
* API (Edge-safe — no node:crypto), expose it on the inbound `x-nonce` request
* header, and set a nonce-based Content-Security-Policy response header. Next.js
* auto-applies this nonce to its own framework <script> tags when the `x-nonce`
* request header is present; the root layout may also read it via `headers()` to
* nonce any manual inline scripts.
*
* 2. Optimistic edge gate (authed prefixes only): redirect anonymous users away from
* authed surfaces. Only checks for the *presence* of a session cookie — real
* session validation (and admin/role checks) happen in the route-group layouts.
*/
export function middleware(req: NextRequest) {
const { pathname, search } = req.nextUrl;
2026-06-20 20:59:03 -04:00
// Per-request nonce (base64). randomUUID is Edge-runtime safe and unguessable.
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https://oaidalleapiprodscus.blob.core.windows.net https://images.unsplash.com",
"media-src 'self'",
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join("; ");
// Optimistic auth gate for the previously-matched authed prefixes only.
const isAuthedPath = AUTHED_PREFIXES.some(
(p) => pathname === p || pathname.startsWith(p + "/")
);
if (isAuthedPath) {
const hasSession = SESSION_COOKIES.some((name) => req.cookies.has(name));
if (!hasSession) {
const signIn = new URL("/sign-in", req.url);
signIn.searchParams.set("redirect", pathname + search);
return NextResponse.redirect(signIn);
}
}
2026-06-20 20:59:03 -04:00
// Forward the nonce to the app via a request header, and set the CSP on the response.
const requestHeaders = new Headers(req.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set("Content-Security-Policy", csp);
const res = NextResponse.next({ request: { headers: requestHeaders } });
res.headers.set("Content-Security-Policy", csp);
return res;
}
export const config = {
matcher: [
2026-06-20 20:59:03 -04:00
// Run on every request EXCEPT static assets so CSP applies app-wide while
// avoiding unnecessary work on prefetched/static files.
{
source: "/((?!_next/static|_next/image|favicon.ico).*)",
missing: [
{ type: "header", key: "next-router-prefetch" },
{ type: "header", key: "purpose", value: "prefetch" },
],
},
],
};