/** * @file AuthFlowRouter.ts * Intelligent authentication flow router * * Determines the correct action for any authentication scenario: * - Public routes: allow access * - Protected routes without session: redirect to login * - Protected routes with session + correct role: allow access * - Protected routes with session + wrong role: show permission error */ import { routes, routeMatchers } from '@/lib/routing/RouteConfig'; import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; import { SearchParamBuilder } from '@/lib/routing/search-params/SearchParamBuilder'; const logger = new ConsoleLogger(); // ============================================================================ // CORE TYPES // ============================================================================ export interface AuthSession { userId: string; role: string; roles?: string[]; } export interface LoginFlowInput { session: AuthSession | null; requestedPath: string; } // ============================================================================ // AUTH FLOW ROUTER // ============================================================================ export class AuthFlowRouter { private readonly _session: AuthSession | null; private readonly _requestedPath: string; private readonly _isPublic: boolean; private readonly _requiredRoles: string[] | null; private constructor(session: AuthSession | null, requestedPath: string) { this._session = session; this._requestedPath = requestedPath; this._isPublic = routeMatchers.isPublic(requestedPath); this._requiredRoles = routeMatchers.requiresRole(requestedPath); } /** * Factory method */ static create(input: LoginFlowInput): AuthFlowRouter { return new AuthFlowRouter(input.session, input.requestedPath); } /** * Check if user has required roles */ private hasRequiredRoles(): boolean { logger.info('[AuthFlowRouter] Checking required roles', { hasSession: !!this._session, requiredRoles: this._requiredRoles, userRole: this._session?.role, userRoles: this._session?.roles }); if (!this._session) { logger.info('[AuthFlowRouter] No session, returning false'); return false; } if (!this._requiredRoles) { logger.info('[AuthFlowRouter] No required roles, returning true'); return true; } const userRoles = this._session.roles || [this._session.role]; const hasRoles = this._requiredRoles.some(role => userRoles.includes(role)); logger.info('[AuthFlowRouter] Role check result', { userRoles, requiredRoles: this._requiredRoles, hasRoles }); return hasRoles; } /** * Get the action to take - pure function */ getAction(): AuthAction { logger.info('[AuthFlowRouter] getAction called', { requestedPath: this._requestedPath, isPublic: this._isPublic, hasSession: !!this._session, requiredRoles: this._requiredRoles }); // Public route - always accessible if (this._isPublic) { logger.info('[AuthFlowRouter] Public route, showing page'); return { type: AuthActionType.SHOW_PAGE }; } // No session - must login if (!this._session) { logger.info('[AuthFlowRouter] No session, redirecting to login'); return { type: AuthActionType.REDIRECT_TO_LOGIN, returnTo: this._requestedPath }; } // Has session + has required roles if (this.hasRequiredRoles()) { logger.info('[AuthFlowRouter] Has required roles, redirecting to destination'); return { type: AuthActionType.REDIRECT_TO_DESTINATION, path: this._requestedPath }; } // Has session but missing roles logger.info('[AuthFlowRouter] Missing required roles, showing permission error'); return { type: AuthActionType.SHOW_PERMISSION_ERROR, requestedPath: this._requestedPath, userRoles: this._session.roles || [this._session.role], requiredRoles: this._requiredRoles! }; } /** * Get login redirect URL */ getLoginRedirectUrl(): string { const action = this.getAction(); if (action.type === AuthActionType.REDIRECT_TO_LOGIN) { return `${routes.auth.login}${SearchParamBuilder.auth(action.returnTo)}`; } if (action.type === AuthActionType.SHOW_PERMISSION_ERROR) { return `${routes.auth.login}${SearchParamBuilder.auth(action.requestedPath)}`; } throw new Error("Not in login redirect state"); } /** * Get permission error message */ getPermissionError(): string { const action = this.getAction(); if (action.type !== AuthActionType.SHOW_PERMISSION_ERROR) { return `Access denied. Please log in with an account that has the required role.`; } const roleText = action.requiredRoles.join(' or '); return `Access denied. Requires ${roleText} role. Your roles: ${action.userRoles.join(', ')}`; } /** * Debug info */ getDebugInfo() { return { session: this._session ? { userId: this._session.userId, role: this._session.role } : null, requestedPath: this._requestedPath, isPublic: this._isPublic, requiredRoles: this._requiredRoles, action: this.getAction() }; } } // ============================================================================ // ACTION TYPES // ============================================================================ export enum AuthActionType { SHOW_PAGE = "SHOW_PAGE", REDIRECT_TO_LOGIN = "REDIRECT_TO_LOGIN", REDIRECT_TO_DESTINATION = "REDIRECT_TO_DESTINATION", SHOW_PERMISSION_ERROR = "SHOW_PERMISSION_ERROR" } export type AuthAction = | { type: AuthActionType.SHOW_PAGE } | { type: AuthActionType.REDIRECT_TO_LOGIN; returnTo: string } | { type: AuthActionType.REDIRECT_TO_DESTINATION; path: string } | { type: AuthActionType.SHOW_PERMISSION_ERROR; requestedPath: string; userRoles: string[]; requiredRoles: string[] }; // ============================================================================ // MIDDLEWARE HELPER // ============================================================================ export function handleAuthFlow( session: AuthSession | null, requestedPath: string ): { shouldRedirect: boolean; redirectUrl?: string; shouldShowPage?: boolean } { logger.info('[handleAuthFlow] Called', { hasSession: !!session, sessionRole: session?.role, requestedPath }); const router = AuthFlowRouter.create({ session, requestedPath }); const action = router.getAction(); logger.info('[handleAuthFlow] Action determined', { actionType: action.type, action: JSON.stringify(action, null, 2) }); switch (action.type) { case AuthActionType.SHOW_PAGE: logger.info('[handleAuthFlow] Returning SHOW_PAGE'); return { shouldRedirect: false, shouldShowPage: true }; case AuthActionType.REDIRECT_TO_LOGIN: const loginUrl = router.getLoginRedirectUrl(); logger.info('[handleAuthFlow] Returning REDIRECT_TO_LOGIN', { loginUrl }); return { shouldRedirect: true, redirectUrl: loginUrl }; case AuthActionType.REDIRECT_TO_DESTINATION: logger.info('[handleAuthFlow] Returning REDIRECT_TO_DESTINATION'); return { shouldRedirect: false, shouldShowPage: true }; case AuthActionType.SHOW_PERMISSION_ERROR: // Redirect to user's home page instead of login (they're already logged in) const homeUrl = session?.role === 'sponsor' ? routes.sponsor.dashboard : 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 }; } }