middleware fix wip

This commit is contained in:
2026-01-04 12:49:30 +01:00
parent 729d95cd73
commit 691e6e2c7e
10 changed files with 741 additions and 152 deletions

View File

@@ -1,27 +1,33 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { SessionGateway } from '@/lib/gateways/SessionGateway';
import { routes, routeMatchers } from '@/lib/routing/RouteConfig';
import { handleAuthFlow } from '@/lib/auth/AuthFlowRouter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { routeMatchers } from '@/lib/routing/RouteConfig';
const logger = new ConsoleLogger();
/**
* 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
*
* Uses UnifiedLoginStateMachine for deterministic, type-safe authentication flow
*/
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const cookieHeader = request.headers.get('cookie') || '';
// Debug logging
console.log(`[MIDDLEWARE] Processing request for path: ${pathname}`);
logger.info('[MIDDLEWARE] ========== REQUEST START ==========');
logger.info('[MIDDLEWARE] Request details', {
pathname,
method: request.method,
url: request.url,
cookieHeaderLength: cookieHeader.length,
cookiePreview: cookieHeader.substring(0, 100) + (cookieHeader.length > 100 ? '...' : '')
});
// Handle /sponsor root redirect to /sponsor/dashboard in middleware to preserve cookies
// Handle /sponsor root redirect (preserves cookies)
if (pathname === '/sponsor') {
console.log(`[MIDDLEWARE] Redirecting /sponsor to /sponsor/dashboard`);
logger.info('[MIDDLEWARE] Redirecting /sponsor /sponsor/dashboard');
return NextResponse.redirect(new URL('/sponsor/dashboard', request.url));
}
@@ -29,118 +35,77 @@ export async function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set('x-pathname', pathname);
// Get session first (needed for all auth-related decisions)
// Get session
logger.info('[MIDDLEWARE] Fetching session...');
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);
logger.info('[MIDDLEWARE] Session fetched', {
hasSession: !!session,
userId: session?.user?.userId,
role: session?.user?.role,
sessionData: session ? JSON.stringify(session, null, 2) : 'null'
});
// Convert session to state machine format
const authSession = session ? {
userId: session.user?.userId || '',
role: session.user?.role || 'driver',
roles: session.user?.role ? [session.user.role] : ['driver']
} : null;
logger.info('[MIDDLEWARE] Auth session converted', {
authSession: authSession ? JSON.stringify(authSession, null, 2) : 'null'
});
// Debug: Log route classification
const isPublic = routeMatchers.isPublic(pathname);
const requiresRole = routeMatchers.requiresRole(pathname);
logger.info('[MIDDLEWARE] Route classification', {
path: pathname,
isPublic,
requiresRole
});
// Use state machine to determine action
let result;
try {
logger.info('[MIDDLEWARE] Calling handleAuthFlow...');
result = handleAuthFlow(authSession, pathname);
logger.info('[MIDDLEWARE] handleAuthFlow result', {
result: JSON.stringify(result, null, 2)
});
} catch (error) {
logger.error('[MIDDLEWARE] Error in auth flow', error instanceof Error ? error : new Error(String(error)));
// Fallback: redirect to login if there's an error
return NextResponse.redirect(new URL(`/auth/login?returnTo=${encodeURIComponent(pathname)}`, request.url));
}
logger.info('[MIDDLEWARE] Decision summary', {
pathname,
hasSession: !!authSession,
role: authSession?.role,
shouldRedirect: result.shouldRedirect,
redirectUrl: result.redirectUrl
});
if (result.shouldRedirect && result.redirectUrl) {
const redirectUrl = new URL(result.redirectUrl, request.url);
logger.info('[MIDDLEWARE] REDIRECTING', {
from: pathname,
to: redirectUrl.toString()
});
const redirectResponse = NextResponse.redirect(redirectUrl);
logger.info('[MIDDLEWARE] ========== REQUEST END (REDIRECT) ==========');
return redirectResponse;
}
// 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`);
// All checks passed
logger.info('[MIDDLEWARE] ALLOWING ACCESS', { pathname });
logger.info('[MIDDLEWARE] ========== REQUEST END (ALLOW) ==========');
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
*/