import { NextResponse, type NextRequest } from "next/server"; /** * Route gate (Next.js 16 proxy, formerly middleware): redirects anonymous * visitors to /login. This is a UX-level gate only — authoritative checks * happen server-side via requireSession()/requirePermission(). */ // SEC2: die Wiederherstellungs-Abläufe müssen ohne Session erreichbar sein — // der Nutzer ist gerade ausgesperrt. Ihre Absicherung sind Rate-Limit, // Enumeration-Neutralität und single-use-Tokens, nicht dieses Gate. const PUBLIC_PATHS = ["/login", "/api/auth", "/forgot-password", "/reset", "/invite", "/verify-email", "/platform/login", "/api/platform-auth"]; // Plattform-Bereich (getrennte Session/Login): diese Routen werden über das // Plattform-Cookie gegatet und leiten anonyme Besucher auf /platform/login — // NICHT auf die Mandanten-Login-Maske. `/admin` deckt auch `/admin/` ab. const PLATFORM_PATHS = ["/admin", "/admins", "/profile", "/platform", "/templates"]; const matchesAny = (pathname: string, paths: string[]) => paths.some((p) => pathname === p || pathname.startsWith(p + "/")); export function proxy(request: NextRequest) { const { pathname } = request.nextUrl; if (matchesAny(pathname, PUBLIC_PATHS)) { return NextResponse.next(); } // Plattform-Routen: eigenes Cookie, eigener Login. Ohne Tenant-Cookie-Bezug, sonst // landet ein angemeldeter Plattform-Admin fälschlich auf der Mandanten-Login-Maske. if (matchesAny(pathname, PLATFORM_PATHS)) { const hasPlatformCookie = request.cookies.has("platform-authjs.session-token") || request.cookies.has("__Secure-platform-authjs.session-token"); if (!hasPlatformCookie) { return NextResponse.redirect(new URL("/platform/login", request.url)); } return NextResponse.next(); } const hasSessionCookie = request.cookies.has("authjs.session-token") || request.cookies.has("__Secure-authjs.session-token"); if (!hasSessionCookie) { // Versioned API: machine clients (PWA sync, integrations) need a status code, not a login page. if (pathname === "/api/v1" || pathname.startsWith("/api/v1/")) { return NextResponse.json( { error: { code: "unauthorized", message: "Nicht angemeldet." } }, { status: 401, headers: { "Cache-Control": "no-store" } }, ); } const loginUrl = new URL("/login", request.url); if (pathname !== "/") loginUrl.searchParams.set("callbackUrl", pathname); return NextResponse.redirect(loginUrl); } return NextResponse.next(); } export const config = { // Everything except static assets. // // `sw.js` (PWA Service Worker): muss auch mit abgelaufener Sitzung für die Update-Prüfung // erreichbar sein; enthält keine Mandantendaten. // Ergänzt um `webmanifest`: das PWA-Manifest muss auch ohne Session ausgeliefert // werden, sonst bekommt der Browser für /site.webmanifest die Login-Seite statt // JSON. Die Logo-/Favicon-Dateien sind über die Endungen bereits abgedeckt. // Bewusst KEINE Verzeichnis-Ausnahme für `assets/` — das ist zugleich die // App-Route des Asset-Inventars und muss hinter dem Gate bleiben. matcher: [ "/((?!_next/static|_next/image|favicon.ico|site.webmanifest|sw\\.js|.*\\.(?:svg|png|jpg|ico|webmanifest)).*)", ], };