import NextAuth from "next-auth"; import { NextResponse } from "next/server"; import { authConfig } from "@/lib/auth/config"; // Next.js 16 renamed Middleware to Proxy (same mechanism, new filename and // export). Das hier ist die vorderste Tür: wer keine Sitzung hat, landet auf // /login statt in der Anwendung. // // Gebaut wird dafür eine *zweite*, absichtlich unvollständige Auth.js-Instanz // — nur aus lib/auth/config.ts, ohne die Rückrufe aus auth.ts. Der Grund ist // handfest: auth.ts spricht mit der Datenbank, und der Proxy läuft je nach // Betriebsart in einer Umgebung ohne Node-Module. Die Instanz hier liest das // Sitzungscookie und sonst nichts. // // Deshalb prüft der Proxy auch nur, *ob* jemand angemeldet ist — nicht mehr, // ob die Person HR-Rechte hat. Das ist keine Lücke, sondern eine Verschiebung // an die Stellen, die es wahrheitsgemäss beantworten können: // // • app/(app)/layout.tsx fragt profiles bei jedem Aufbau frisch ab und // leitet auf /login?error=no_hr_access um, // • die Route Handler unter /api/export/* tun dasselbe über requireHrUser(), // • und darunter, unabhängig von allem Anwendungscode, entscheiden die // RLS-Policies über is_hr_user(). // // Die Alternative — role und is_active ins Sitzungstoken schreiben — hätte // den Proxy schneller gemacht und dafür eine Behauptung eingefroren: eine // entzogene Freischaltung wirkte erst mit dem nächsten Token. Bei einer // Personalanwendung ist das die falsche Richtung. // Als Funktion übergeben, nicht als Objekt: siehe authConfig(). const { auth } = NextAuth(() => authConfig()); const gate = auth((request) => { const { pathname } = request.nextUrl; // Auth.js' eigene Endpunkte müssen durch, bevor es eine Sitzung gibt — // dort entsteht sie ja erst. Ohne diese Ausnahme leitet der Proxy den // Rückweg aus Entra nach /login um und die Anmeldung kommt nie zustande. if (pathname.startsWith("/api/auth")) return NextResponse.next(); const isLoginRoute = pathname.startsWith("/login"); const isSignedIn = Boolean(request.auth?.user?.id); if (!isSignedIn) { if (isLoginRoute) return NextResponse.next(); const url = request.nextUrl.clone(); url.pathname = "/login"; url.search = ""; return NextResponse.redirect(url); } if (isLoginRoute) { // Angemeldet und trotzdem auf /login: nur weiterschicken, wenn keine // Meldung ansteht. Sonst geriete jemand ohne HR-Freischaltung in eine // Schleife — das Layout leitet nach /login?error=no_hr_access, der Proxy // zurück auf /, das Layout wieder … und der Grund wäre nie zu lesen. if (request.nextUrl.searchParams.has("error")) return NextResponse.next(); const url = request.nextUrl.clone(); url.pathname = "/"; url.search = ""; return NextResponse.redirect(url); } return NextResponse.next(); }); // Zwei Eigenheiten auf einmal, beide erst beim Ausprobieren aufgefallen: // // 1. Next.js sucht hier eine *Funktionsdeklaration* namens `proxy` (oder // einen Default-Export) und erkennt `export const proxy = auth(…)` nicht. // Jede Anfrage lief in einen 404 — und `next build` meldete Erfolg und // listete den Proxy sogar auf. // // 2. In der Funktionsform liefert `auth(handler)` den Handler erst als // Zusage. Ohne `await` steht hier ein Promise, und der Aufruf scheitert // mit „gate is not a function". Auf einem gewöhnlichen Funktionswert ist // `await` wirkungslos, das `await` ist also in beiden Fällen richtig. export async function proxy(...args: Parameters>) { return (await gate)(...args); } // ═══ Was der Proxy gar nicht erst sieht ═══ // // Der Abgleich ist eine Verneinung: alles ausser dem hier Aufgezaehlten laeuft // durch den Gate. Das ist die richtige Richtung — eine vergessene Route ist // dann geschuetzt und nicht offen. // // Es fehlten allerdings die statischen Dateien aus public/. Eine Anfrage nach // /brand/manner-logo.png lief damit in den Gate, fand keine Sitzung und // bekam eine Umleitung nach /login. Der Browser bekommt auf ein also // HTML statt eines Bildes und zeigt den Ersatztext. // // Aufgefallen ist es nur an einer Stelle, und zwar an der einzigen, an der es // auffallen konnte: die Anmeldeseite ist die einzige Seite, die jemand ohne // Sitzung zu sehen bekommt — und ausgerechnet dort steht das Logo. Ueberall // sonst ist man angemeldet, der Gate laesst das Bild durch, und es erscheint. // Daher auch das Sprunghafte: einmal angemeldet geladen, liegt es im // Zwischenspeicher des Browsers und erscheint auf /login weiter, bis jemand // hart neu laedt. // // Aufgenommen wird nur, was eine nicht angemeldete Person auf der // Anmeldeseite braucht, und jeder Eintrag einzeln statt einer Regel ueber // Dateiendungen: „alles mit einem Punkt darin" haette auch Routen // durchgelassen, die keine Datei sind. // // _next/static, _next/image die Ausgabe des Bauwerkzeugs // favicon.ico, icon.svg, die Symbole, die der Browser von sich aus holt // apple-icon.png // manifest.webmanifest dito, fuer die Installation als App // brand/ Logo und App-Symbole aus public/brand // // In public/ liegt sonst nichts; personenbezogen ist dort nichts und darf // dort auch nie etwas liegen — alles, was Daten fuehrt, geht durch withUser() // und die Policies. export const config = { matcher: [ "/((?!_next/static|_next/image|favicon\\.ico|icon\\.svg|apple-icon\\.png|manifest\\.webmanifest|brand/).*)", ], };