242 lines
7.9 KiB
TypeScript
242 lines
7.9 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 } 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 isAdmin = session?.role === 'admin' ||
|
|
session?.role === 'league-admin' ||
|
|
session?.role === 'system-owner' ||
|
|
session?.role === 'super-admin';
|
|
|
|
const homeUrl = session?.role === 'sponsor' ? routes.sponsor.dashboard :
|
|
isAdmin ? 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 };
|
|
}
|
|
}
|