Replaces the dead hand-rolled buildCsp() draft in next.config.ts (never wired in — its script-src had no 'unsafe-inline'/nonce, which blocked Next.js's own hydration script and blanked the whole app when tested live on 2026-08-10) with Clerk's official contentSecurityPolicy option on clerkMiddleware(). This is Clerk's documented workaround: its default directives already include the permissive script-src Next.js App Router needs, merged with our app-specific img-src/connect-src/media-src/ font-src/frame-ancestors/object-src/base-uri additions (Supabase, Mux, litix). Ships as Content-Security-Policy-Report-Only (reportOnly: true) with both reporting mechanisms wired to a new /api/csp-report route handler: Clerk's reportTo (Reporting API) and a classic report-uri (for browsers without Reporting API support). The route normalizes both report shapes, rate-limits by IP, and forwards violations to Sentry/GlitchTip as warnings — never blocks the request. Deliberately NOT enforced yet — move to reportOnly: false only after a clean observation period with zero unexpected violations. Gitea #5
103 lines
3.2 KiB
TypeScript
103 lines
3.2 KiB
TypeScript
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
|
|
import { NextResponse } from "next/server";
|
|
|
|
const isPublicRoute = createRouteMatcher([
|
|
"/",
|
|
"/sign-up(.*)",
|
|
"/log-in(.*)",
|
|
"/forgot-password(.*)",
|
|
"/reset-password(.*)",
|
|
"/verify-email(.*)",
|
|
"/health-card/(.*)",
|
|
"/api/gdpr-export/(.*)",
|
|
"/opengraph-image",
|
|
"/impressum",
|
|
"/datenschutz",
|
|
"/nutzungsbedingungen",
|
|
"/join(.*)",
|
|
"/api/webhooks/(.*)",
|
|
"/api/auth/(.*)",
|
|
"/api/cron/(.*)",
|
|
"/api/csp-report",
|
|
]);
|
|
|
|
function getSupabaseHostname(): string {
|
|
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
if (url) {
|
|
try {
|
|
return new URL(url).hostname;
|
|
} catch {
|
|
/* fall through */
|
|
}
|
|
}
|
|
return "*.supabase.co";
|
|
}
|
|
|
|
export default clerkMiddleware(
|
|
async (auth, request) => {
|
|
const { pathname } = request.nextUrl;
|
|
|
|
// Invite gate: block /sign-up unless the visitor has a valid invite cookie.
|
|
// Set INVITE_REQUIRED=false in env to disable for the live/post-beta launch.
|
|
if (pathname.startsWith("/sign-up")) {
|
|
const inviteRequired = process.env.INVITE_REQUIRED !== "false";
|
|
if (inviteRequired && !request.cookies.has("pf_invite")) {
|
|
return NextResponse.redirect(new URL("/join", request.url));
|
|
}
|
|
}
|
|
|
|
// Admin panel: validate secret path segment before anything else.
|
|
// Returns 404 (not 403) — the panel's existence must not be revealed.
|
|
if (pathname.startsWith("/p/")) {
|
|
const segments = pathname.split("/");
|
|
const providedSecret = segments[2] ?? "";
|
|
const adminSecret = process.env.ADMIN_SECRET ?? "";
|
|
|
|
if (!adminSecret || providedSecret !== adminSecret) {
|
|
return new NextResponse(null, { status: 404 });
|
|
}
|
|
}
|
|
|
|
if (!isPublicRoute(request)) {
|
|
await auth.protect();
|
|
}
|
|
},
|
|
{
|
|
// Report-Only for now — an earlier enforced attempt blanked the whole
|
|
// app because ClerkProvider crashes uncaught when a required resource
|
|
// gets blocked mid-init. Clerk's own contentSecurityPolicy option (not
|
|
// the hand-rolled next.config.ts draft this replaces) already ships the
|
|
// 'unsafe-inline'/https: script-src Next.js needs to hydrate, which is
|
|
// what the hand-rolled version was missing. Move to enforced only after
|
|
// a clean report-only period — see /api/csp-report for the violations.
|
|
contentSecurityPolicy: {
|
|
reportOnly: true,
|
|
reportTo: "/api/csp-report",
|
|
directives: {
|
|
"img-src": [`https://${getSupabaseHostname()}`, "https://image.mux.com"],
|
|
"connect-src": [
|
|
`https://${getSupabaseHostname()}`,
|
|
"https://stream.mux.com",
|
|
"https://image.mux.com",
|
|
"https://litix.io",
|
|
"https://storage.googleapis.com",
|
|
],
|
|
"media-src": ["self", "blob:", "https://stream.mux.com"],
|
|
"font-src": ["self", "data:"],
|
|
"frame-ancestors": ["none"],
|
|
"object-src": ["none"],
|
|
"base-uri": ["self"],
|
|
"report-uri": ["/api/csp-report"],
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
export const config = {
|
|
matcher: [
|
|
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
|
|
"/(api|trpc)(.*)",
|
|
"/__clerk/(.*)",
|
|
],
|
|
};
|