128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
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<string, string> = {
|
|
'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)$).*)',
|
|
],
|
|
}; |