Files
gridpilot.gg/apps/website/middleware.ts
2026-01-04 01:45:14 +01:00

159 lines
5.8 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;
// Debug logging
console.log(`[MIDDLEWARE] Processing request for path: ${pathname}`);
// Handle /sponsor root redirect to /sponsor/dashboard in middleware to preserve cookies
if (pathname === '/sponsor') {
console.log(`[MIDDLEWARE] Redirecting /sponsor to /sponsor/dashboard`);
return NextResponse.redirect(new URL('/sponsor/dashboard', request.url));
}
// 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.getSessionFromRequest(request);
console.log(`[MIDDLEWARE] Session retrieved:`, session ? 'Session found' : 'No session');
if (session) {
console.log(`[MIDDLEWARE] User role:`, session.user?.role);
}
// Auth pages (login, signup, etc.) - handle before public check
if (routeMatchers.isInGroup(pathname, 'auth')) {
if (session) {
// Check if user was redirected here due to insufficient permissions
const returnTo = request.nextUrl.searchParams.get('returnTo');
if (returnTo) {
// User has a session but insufficient permissions for the returnTo route
// Allow them to see the login page (they may need to switch accounts)
console.log(`[MIDDLEWARE] Authenticated user at login with returnTo, allowing access`);
return response;
}
// User is authenticated and navigated to auth page directly, redirect away
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)) {
console.log(`[MIDDLEWARE] Route is public, allowing access`);
return response;
}
// Protected routes (require authentication)
if (!session) {
// No session, redirect to login
console.log(`[MIDDLEWARE] No session, redirecting 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}`;
}
console.log(`[MIDDLEWARE] Redirecting to:`, redirectUrl.toString());
return NextResponse.redirect(redirectUrl);
}
// Role-based access control
const requiredRoles = routeMatchers.requiresRole(pathname);
console.log(`[MIDDLEWARE] Required roles for ${pathname}:`, requiredRoles);
if (requiredRoles) {
const userRole = session.user?.role;
console.log(`[MIDDLEWARE] User role:`, userRole);
if (!userRole || !requiredRoles.includes(userRole)) {
// User doesn't have required role or no role at all, redirect to login
console.log(`[MIDDLEWARE] User doesn't have required role, redirecting 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}`;
}
console.log(`[MIDDLEWARE] Redirecting to:`, redirectUrl.toString());
return NextResponse.redirect(redirectUrl);
}
}
// All checks passed, allow access
console.log(`[MIDDLEWARE] All checks passed, allowing 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)$).*)',
],
};