51c541ad22
Cross-cutting input-validation, isolation, and DoS-resistance fixes across the app, API, billing, queue, and infra layers. - Runtime validation (zod) for client-supplied admin actions (role/plan/ limits), series generation index, and all pg-boss queue payloads - Auth: require email verification before sign-in; reject weak/placeholder/ short BETTER_AUTH_SECRET in production - Billing: sanitize Stripe/PayPal errors (log server-side, generic to client); race-safe subscription upsert; only count "processed" webhook events as handled; verify org membership in getEffectivePlan to block plan escalation - Series generation: reserve usage up front and refund on failure; bill the owning org, not the caller's active org - Injection defenses: HTML-escape user fields in emails, strip CR/LF from subject/recipient, validate ElevenLabs voiceId before URL interpolation - Media routes: stream off disk instead of buffering whole files; rate-limit anonymous public audio/cover endpoints by client IP
89 lines
3.3 KiB
TypeScript
89 lines
3.3 KiB
TypeScript
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"];
|
|
|
|
// 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",
|
|
];
|
|
|
|
/**
|
|
* 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;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// 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: [
|
|
// 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" },
|
|
],
|
|
},
|
|
],
|
|
};
|