import { NextRequest, NextResponse } from 'next/server'; import { jwtVerify } from 'jose'; /** * Next.js middleware for frontend route protection (Pattern 4). * * Validates JWT session cookie on every request. Redirects to /login * if missing or invalid. This is an optimistic check -- the API still * validates the JWT independently on every request. */ const publicRoutes = ['/login', '/reset-password']; function getSecret() { const secret = process.env.JWT_SECRET || process.env.SESSION_SECRET; if (!secret) { // In development, allow a fallback to prevent startup crashes // when env vars are not yet configured return new TextEncoder().encode('development-secret-change-me'); } return new TextEncoder().encode(secret); } export async function middleware(req: NextRequest) { const path = req.nextUrl.pathname; // Allow public routes without authentication if (publicRoutes.some((route) => path.startsWith(route))) { return NextResponse.next(); } // Skip static assets and API routes (handled by NestJS) if ( path.startsWith('/_next/static') || path.startsWith('/_next/image') || path.startsWith('/favicon.ico') || path.startsWith('/api') ) { return NextResponse.next(); } // Read session cookie const session = req.cookies.get('session')?.value; if (!session) { return NextResponse.redirect(new URL('/login', req.nextUrl)); } try { const { payload } = await jwtVerify(session, getSecret(), { algorithms: ['HS256'], }); // D-06: Force password change redirect if ( payload.mustChangePassword === true && !path.startsWith('/change-password') ) { return NextResponse.redirect(new URL('/change-password', req.nextUrl)); } return NextResponse.next(); } catch { // JWT verification failed -- clear stale cookie and redirect to login const response = NextResponse.redirect(new URL('/login', req.nextUrl)); response.cookies.delete('session'); return response; } } export const config = { matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], };