- proxy: /sw.js vom Session-Gate ausgenommen (Update-Prüfung auch bei abgelaufener Sitzung; enthält keine Mandantendaten) – gemeldet von L7 - next.config: /sw.js mit Cache-Control no-cache/no-store, Service-Worker-Allowed / - scripts/smoke-auth.ts: Session-Cookie über finalizeIdentityLogin + next-auth/jwt encode (ohne Passworteingabe), prüft Backoffice- und Monteur-Seiten Nachweis: /sw.js anonym 200 + no-cache; Smoke 19/19 Seiten grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
75 lines
3.2 KiB
TypeScript
75 lines
3.2 KiB
TypeScript
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/<id>` 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)).*)",
|
|
],
|
|
};
|