middleware fix wip
This commit is contained in:
@@ -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',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user