Files
gridpilot.gg/apps/website/lib/auth/AuthFlowRouter.ts
2026-01-04 23:02:28 +01:00

237 lines
7.7 KiB
TypeScript

/**
* @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, RouteGroup } from '@/lib/routing/RouteConfig';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
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) {
const params = new URLSearchParams({ returnTo: action.returnTo });
return `${routes.auth.login}?${params.toString()}`;
}
if (action.type === AuthActionType.SHOW_PERMISSION_ERROR) {
const params = new URLSearchParams({ returnTo: action.requestedPath });
return `${routes.auth.login}?${params.toString()}`;
}
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 };
}
}