/** * Middleware: schuetzt alle privaten Bereiche (plan.md Abschnitt 13). * * Nicht angemeldete Benutzer werden zur Anmeldung geleitet, bevor * ueberhaupt serverseitige Daten geladen werden. Die eigentliche * Berechtigungspruefung bleibt zusaetzlich in den Routen/Seiten * (Defense in Depth) - die Middleware ist nur die erste Schranke. */ import { NextResponse, type NextRequest } from 'next/server'; import { jwtVerify } from 'jose'; const SESSION_COOKIE_NAME = 'calendar_session'; /** Oeffentliche Pfade, die ohne Session erreichbar sind. */ const PUBLIC_PATHS = new Set([ '/login', '/register', '/forgot-password', // SSO-Einstieg der MultiToolApp-Plattform: Authentifizierung erfolgt // ueber das Hub-Token im Query-Parameter, nicht ueber die Session. '/auth/hub', // Modul-Contract: Manifest muss ohne Session abrufbar sein. '/api/manifest', ]); function isPublicPath(pathname: string): boolean { if (PUBLIC_PATHS.has(pathname)) { return true; } // Invite-Annahme ist oeffentlich (plan.md Abschnitt 3). if (pathname.startsWith('/invite/')) { return true; } // Statische Assets und API-Auth-Routen. if (pathname.startsWith('/_next') || pathname.startsWith('/api/auth')) { return true; } // Modul-Contract: .well-known-Pfad (wird per Rewrite auf /api/manifest gemappt). if (pathname.startsWith('/.well-known/')) { return true; } return false; } export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; if (process.env.MPM_AUTH_MODE === 'true') { const userId = request.headers.get('x-kalendartool-mpm-user-id'); const role = request.headers.get('x-kalendartool-mpm-role'); if (userId && role && ['ADMIN', 'USER'].includes(role)) { const basePath = (process.env.HUB_BASE_PATH ?? '').replace(/\/$/, ''); const localPath = basePath && pathname.startsWith(basePath) ? pathname.slice(basePath.length) || '/' : pathname; if (['/login', '/register', '/forgot-password', '/auth/hub'].includes(localPath)) { const dashboardUrl = request.nextUrl.clone(); dashboardUrl.pathname = `${basePath}/dashboard`; return NextResponse.redirect(dashboardUrl); } if (localPath.startsWith('/api/auth/')) { return NextResponse.json({ statusCode: 404, message: 'Nicht gefunden' }, { status: 404 }); } return NextResponse.next(); } return NextResponse.json({ statusCode: 401, message: 'Nicht authentifiziert' }, { status: 401 }); } if (isPublicPath(pathname)) { return NextResponse.next(); } const token = request.cookies.get(SESSION_COOKIE_NAME)?.value; if (!token) { const loginUrl = new URL('/login', request.url); return NextResponse.redirect(loginUrl); } try { const secret = new TextEncoder().encode(process.env.SESSION_SECRET ?? ''); if (secret.length === 0) { throw new Error('SESSION_SECRET fehlt.'); } await jwtVerify(token, secret); return NextResponse.next(); } catch { // Ungueltige/abgelaufene Session -> Cookie loeschen und zur Anmeldung. const loginUrl = new URL('/login', request.url); const response = NextResponse.redirect(loginUrl); response.cookies.delete(SESSION_COOKIE_NAME); return response; } } export const config = { // Alles ausser statischen Dateien. matcher: ['/((?!favicon.ico|.*\\.(?:png|jpg|jpeg|svg|ico|webp|txt)).*)'], };