import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { SessionGateway } from '@/lib/gateways/SessionGateway'; import { routes, routeMatchers } from '@/lib/routing/RouteConfig'; /** * Server-side route protection middleware * * This middleware provides comprehensive route protection by: * 1. Setting x-pathname header for layout-level protection * 2. Checking authentication status via SessionGateway * 3. Redirecting unauthenticated users from protected routes * 4. Redirecting authenticated users away from auth pages * 5. Handling role-based access control */ export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; // Set x-pathname header for layout-level protection const response = NextResponse.next(); response.headers.set('x-pathname', pathname); // Get session first (needed for all auth-related decisions) const sessionGateway = new SessionGateway(); const session = await sessionGateway.getSession(); // Auth pages (login, signup, etc.) - handle before public check if (routeMatchers.isInGroup(pathname, 'auth')) { if (session) { // User is authenticated, redirect away from auth page const role = session.user?.role || 'driver'; const redirectPath = getHomePathForRole(role); // Preserve locale if present in the original path const localeMatch = pathname.match(/^\/([a-z]{2})\//); if (localeMatch) { const locale = localeMatch[1]; return NextResponse.redirect(new URL(`/${locale}${redirectPath}`, request.url)); } return NextResponse.redirect(new URL(redirectPath, request.url)); } // Unauthenticated users can access auth pages return response; } // Public routes (no auth required, but not auth pages) if (routeMatchers.isPublic(pathname)) { return response; } // Protected routes (require authentication) if (!session) { // No session, redirect to login // Preserve locale if present in the path const localeMatch = pathname.match(/^\/([a-z]{2})\//); const locale = localeMatch ? localeMatch[1] : null; const redirectUrl = new URL(routes.auth.login, request.url); redirectUrl.searchParams.set('returnTo', pathname); // If locale is present, include it in the redirect if (locale) { redirectUrl.pathname = `/${locale}${redirectUrl.pathname}`; } return NextResponse.redirect(redirectUrl); } // Role-based access control const requiredRoles = routeMatchers.requiresRole(pathname); if (requiredRoles) { const userRole = session.user?.role; if (!userRole || !requiredRoles.includes(userRole)) { // User doesn't have required role or no role at all, redirect to login // Preserve locale if present in the path const localeMatch = pathname.match(/^\/([a-z]{2})\//); const locale = localeMatch ? localeMatch[1] : null; const redirectUrl = new URL(routes.auth.login, request.url); redirectUrl.searchParams.set('returnTo', pathname); if (locale) { redirectUrl.pathname = `/${locale}${redirectUrl.pathname}`; } return NextResponse.redirect(redirectUrl); } } // All checks passed, allow access return response; } /** * Get the home path for a specific role */ function getHomePathForRole(role: string): string { const roleHomeMap: Record = { 'driver': routes.protected.dashboard, 'sponsor': routes.sponsor.dashboard, 'league-admin': routes.admin.root, 'league-steward': routes.admin.root, 'league-owner': routes.admin.root, 'system-owner': routes.admin.root, 'super-admin': routes.admin.root, }; return roleHomeMap[role] || routes.protected.dashboard; } /** * Configure which routes the middleware should run on */ export const config = { matcher: [ /* * Match all request paths except: * - _next/static (static files) * - _next/image (image optimization files) * - _next/data (Next.js data requests) * - favicon.ico (favicon file) * - Files with extensions (static assets) */ '/((?!_next/static|_next/image|_next/data|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|mp4|webm|mov|avi)$).*)', ], };