middleware fix wip

This commit is contained in:
2026-01-04 23:02:28 +01:00
parent 691e6e2c7e
commit cd3d9ae34f
8 changed files with 74 additions and 13 deletions

View File

@@ -38,9 +38,9 @@ export class SeedDemoUsers {
{ {
email: 'demo.sponsor@example.com', email: 'demo.sponsor@example.com',
password: 'Demo1234!', password: 'Demo1234!',
needsAdminUser: false, needsAdminUser: true,
needsPrimaryDriverId: false, needsPrimaryDriverId: false,
roles: ['user'], roles: ['sponsor'],
displayName: 'Jane Sponsor', displayName: 'Jane Sponsor',
}, },
{ {

View File

@@ -12,11 +12,15 @@ interface AdminLayoutProps {
* Uses RouteGuard to enforce access control server-side. * Uses RouteGuard to enforce access control server-side.
*/ */
export default async function AdminLayout({ children }: AdminLayoutProps) { export default async function AdminLayout({ children }: AdminLayoutProps) {
console.log('[ADMIN LAYOUT] ========== ADMIN LAYOUT CALLED ==========');
const headerStore = await headers(); const headerStore = await headers();
const pathname = headerStore.get('x-pathname') || '/'; const pathname = headerStore.get('x-pathname') || '/';
console.log('[ADMIN LAYOUT] Pathname:', pathname);
const guard = createRouteGuard(); const guard = createRouteGuard();
console.log('[ADMIN LAYOUT] About to call guard.enforce');
await guard.enforce({ pathname }); await guard.enforce({ pathname });
console.log('[ADMIN LAYOUT] guard.enforce completed successfully');
return ( return (
<div className="min-h-screen bg-deep-graphite"> <div className="min-h-screen bg-deep-graphite">

View File

@@ -47,6 +47,18 @@ export class EnhancedErrorBoundary extends Component<Props, State> {
} }
static getDerivedStateFromError(error: Error): State { static getDerivedStateFromError(error: Error): State {
// Don't catch Next.js navigation errors (redirect, notFound, etc.)
if (error && typeof error === 'object' && 'digest' in error) {
const digest = (error as any).digest;
if (typeof digest === 'string' && (
digest.startsWith('NEXT_REDIRECT') ||
digest.startsWith('NEXT_NOT_FOUND')
)) {
// Re-throw Next.js navigation errors so they can be handled properly
throw error;
}
}
return { return {
hasError: true, hasError: true,
error, error,
@@ -56,6 +68,18 @@ export class EnhancedErrorBoundary extends Component<Props, State> {
} }
componentDidCatch(error: Error, errorInfo: ErrorInfo): void { componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
// Don't catch Next.js navigation errors (redirect, notFound, etc.)
if (error && typeof error === 'object' && 'digest' in error) {
const digest = (error as any).digest;
if (typeof digest === 'string' && (
digest.startsWith('NEXT_REDIRECT') ||
digest.startsWith('NEXT_NOT_FOUND')
)) {
// Re-throw Next.js navigation errors so they can be handled properly
throw error;
}
}
// Add to React error history // Add to React error history
const reactErrors = (window as any).__GRIDPILOT_REACT_ERRORS__ || []; const reactErrors = (window as any).__GRIDPILOT_REACT_ERRORS__ || [];
reactErrors.push({ reactErrors.push({

View File

@@ -227,8 +227,11 @@ export function handleAuthFlow(
return { shouldRedirect: false, shouldShowPage: true }; return { shouldRedirect: false, shouldShowPage: true };
case AuthActionType.SHOW_PERMISSION_ERROR: case AuthActionType.SHOW_PERMISSION_ERROR:
const errorUrl = router.getLoginRedirectUrl(); // Redirect to user's home page instead of login (they're already logged in)
logger.info('[handleAuthFlow] Returning SHOW_PERMISSION_ERROR', { errorUrl }); const homeUrl = session?.role === 'sponsor' ? routes.sponsor.dashboard :
return { shouldRedirect: true, redirectUrl: errorUrl }; session?.role === 'admin' ? routes.admin.root :
routes.protected.dashboard;
logger.info('[handleAuthFlow] Returning SHOW_PERMISSION_ERROR, redirecting to home', { homeUrl, userRole: session?.role });
return { shouldRedirect: true, redirectUrl: homeUrl };
} }
} }

View File

@@ -49,7 +49,9 @@ export class RouteAccessPolicy {
*/ */
requiredRoles(logicalPathname: string): string[] | null { requiredRoles(logicalPathname: string): string[] | null {
// Use catalog's role-based access method // Use catalog's role-based access method
return this.catalog.getRequiredRoles(logicalPathname); const roles = this.catalog.getRequiredRoles(logicalPathname);
console.log(`[RouteAccessPolicy] requiredRoles for ${logicalPathname}:`, roles);
return roles;
} }
/** /**

View File

@@ -4,6 +4,9 @@ import { RouteAccessPolicy } from './RouteAccessPolicy';
import { SessionGateway } from '../gateways/SessionGateway'; import { SessionGateway } from '../gateways/SessionGateway';
import { AuthRedirectBuilder } from './AuthRedirectBuilder'; import { AuthRedirectBuilder } from './AuthRedirectBuilder';
import type { AuthSessionDTO } from '../types/generated/AuthSessionDTO'; import type { AuthSessionDTO } from '../types/generated/AuthSessionDTO';
import { ConsoleLogger } from '../infrastructure/logging/ConsoleLogger';
const logger = new ConsoleLogger();
export class RouteGuard { export class RouteGuard {
constructor( constructor(
@@ -14,43 +17,55 @@ export class RouteGuard {
) {} ) {}
async enforce({ pathname }: { pathname: string }): Promise<void> { async enforce({ pathname }: { pathname: string }): Promise<void> {
logger.info('[RouteGuard] enforce called', { pathname });
// Step 1: Interpret the pathname // Step 1: Interpret the pathname
const { logicalPathname } = this.interpreter.interpret(pathname); const { logicalPathname } = this.interpreter.interpret(pathname);
logger.info('[RouteGuard] logicalPathname', { logicalPathname });
// Step 2: Check if public non-auth page // Step 2: Check if public non-auth page
if (this.policy.isPublic(logicalPathname) && !this.policy.isAuthPage(logicalPathname)) { if (this.policy.isPublic(logicalPathname) && !this.policy.isAuthPage(logicalPathname)) {
logger.info('[RouteGuard] Public non-auth page, allowing access');
return; // Allow access return; // Allow access
} }
// Step 3: Handle auth pages // Step 3: Handle auth pages
if (this.policy.isAuthPage(logicalPathname)) { if (this.policy.isAuthPage(logicalPathname)) {
logger.info('[RouteGuard] Auth page detected');
const session = await this.gateway.getSession(); const session = await this.gateway.getSession();
if (session) { if (session) {
// User is logged in, redirect away from auth page // User is logged in, redirect away from auth page
const redirectPath = this.builder.awayFromAuthPage({ session, currentPathname: pathname }); const redirectPath = this.builder.awayFromAuthPage({ session, currentPathname: pathname });
logger.info('[RouteGuard] Redirecting away from auth page', { redirectPath });
redirect(redirectPath); redirect(redirectPath);
} }
// No session, allow access to auth page // No session, allow access to auth page
logger.info('[RouteGuard] No session, allowing access to auth page');
return; return;
} }
// Step 4: Handle protected pages // Step 4: Handle protected pages
const session = await this.gateway.getSession(); const session = await this.gateway.getSession();
logger.info('[RouteGuard] Protected page, session', { hasSession: !!session, role: session?.user?.role, sessionData: JSON.stringify(session, null, 2) });
// No session, redirect to login // No session, redirect to login
if (!session) { if (!session) {
const loginPath = this.builder.toLogin({ currentPathname: pathname }); const loginPath = this.builder.toLogin({ currentPathname: pathname });
logger.info('[RouteGuard] No session, redirecting to login', { loginPath });
redirect(loginPath); redirect(loginPath);
} }
// Check required roles // Check required roles
const reqRoles = this.policy.requiredRoles(logicalPathname); const reqRoles = this.policy.requiredRoles(logicalPathname);
logger.info('[RouteGuard] Checking required roles', { reqRoles, userRole: session.user?.role });
if (reqRoles && session.user?.role && !reqRoles.includes(session.user.role)) { if (reqRoles && session.user?.role && !reqRoles.includes(session.user.role)) {
const loginPath = this.builder.toLogin({ currentPathname: pathname }); const loginPath = this.builder.toLogin({ currentPathname: pathname });
logger.info('[RouteGuard] Role mismatch, redirecting to login', { loginPath, reqRoles, userRole: session.user.role });
redirect(loginPath); redirect(loginPath);
} }
// All checks passed, allow access // All checks passed, allow access
logger.info('[RouteGuard] All checks passed, allowing access');
return; return;
} }
} }

View File

@@ -78,7 +78,8 @@ export class SessionGateway {
// Parse and return session data // Parse and return session data
const session = await response.json(); const session = await response.json();
console.log(`[SESSION] Session parsed successfully`); console.log(`[SESSION] Session parsed successfully:`, JSON.stringify(session, null, 2));
console.log(`[SESSION] Session user role:`, session?.user?.role);
return session as AuthSessionDTO; return session as AuthSessionDTO;
} catch (error) { } catch (error) {
console.log(`[SESSION] Error occurred:`, error); console.log(`[SESSION] Error occurred:`, error);

View File

@@ -32,6 +32,13 @@ test.describe('Website Pages - TypeORM Integration', () => {
for (const route of publicRoutes) { for (const route of publicRoutes) {
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params); const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
const response = await page.goto(`${WEBSITE_BASE_URL}${path}`); const response = await page.goto(`${WEBSITE_BASE_URL}${path}`);
const status = response?.status();
const finalUrl = page.url();
console.log(`[TEST DEBUG] Public route - Path: ${path}, Status: ${status}, Final URL: ${finalUrl}`);
if (status === 500) {
console.log(`[TEST DEBUG] 500 error on ${path} - Page title: ${await page.title()}`);
}
// Should load successfully or show 404 page // Should load successfully or show 404 page
expect(response?.ok() || response?.status() === 404).toBeTruthy(); expect(response?.ok() || response?.status() === 404).toBeTruthy();
@@ -59,11 +66,16 @@ test.describe('Website Pages - TypeORM Integration', () => {
for (const route of adminRoutes) { for (const route of adminRoutes) {
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params); const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
// Regular auth user should be blocked // Regular auth user should be redirected to their home page (dashboard)
{ {
const auth = await WebsiteAuthManager.createAuthContext(browser, request, 'auth'); const auth = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
await auth.page.goto(`${WEBSITE_BASE_URL}${path}`); const response = await auth.page.goto(`${WEBSITE_BASE_URL}${path}`);
expect(auth.page.url().includes('login')).toBeTruthy(); const finalUrl = auth.page.url();
console.log(`[TEST DEBUG] Admin route test - Path: ${path}`);
console.log(`[TEST DEBUG] Response status: ${response?.status()}`);
console.log(`[TEST DEBUG] Final URL: ${finalUrl}`);
console.log(`[TEST DEBUG] Page title: ${await auth.page.title()}`);
expect(auth.page.url().includes('dashboard')).toBeTruthy();
await auth.context.close(); await auth.context.close();
} }
@@ -84,14 +96,14 @@ test.describe('Website Pages - TypeORM Integration', () => {
for (const route of sponsorRoutes) { for (const route of sponsorRoutes) {
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params); const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
// Regular auth user should be blocked // Regular auth user should be redirected to their home page (dashboard)
{ {
const auth = await WebsiteAuthManager.createAuthContext(browser, request, 'auth'); const auth = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
await auth.page.goto(`${WEBSITE_BASE_URL}${path}`); await auth.page.goto(`${WEBSITE_BASE_URL}${path}`);
const finalUrl = auth.page.url(); const finalUrl = auth.page.url();
console.log(`[DEBUG] Final URL: ${finalUrl}`); console.log(`[DEBUG] Final URL: ${finalUrl}`);
console.log(`[DEBUG] Includes 'login': ${finalUrl.includes('login')}`); console.log(`[DEBUG] Includes 'dashboard': ${finalUrl.includes('dashboard')}`);
expect(finalUrl.includes('login')).toBeTruthy(); expect(finalUrl.includes('dashboard')).toBeTruthy();
await auth.context.close(); await auth.context.close();
} }