/** * @file RouteConfig.ts * Centralized routing configuration for clean, maintainable paths * * Design Principles: * - Single source of truth for all routes * - i18n-ready: paths can be localized * - Type-safe: compile-time checking * - Easy to refactor: change in one place * - Environment-specific: can vary by mode */ import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; const logger = new ConsoleLogger(); export interface RouteDefinition { path: string; name: string; description?: string; } export interface RouteGroup { auth: { login: string; signup: string; forgotPassword: string; resetPassword: string; }; public: { home: string; leagues: string; drivers: string; teams: string; leaderboards: string; races: string; sponsorSignup: string; }; protected: { dashboard: string; onboarding: string; profile: string; profileSettings: string; profileLeagues: string; profileLiveries: string; profileLiveryUpload: string; profileSponsorshipRequests: string; }; sponsor: { root: string; dashboard: string; billing: string; campaigns: string; leagues: string; leagueDetail: (id: string) => string; settings: string; }; admin: { root: string; users: string; }; league: { detail: (id: string) => string; rosterAdmin: (id: string) => string; rulebook: (id: string) => string; schedule: (id: string) => string; scheduleAdmin: (id: string) => string; settings: (id: string) => string; sponsorships: (id: string) => string; standings: (id: string) => string; stewarding: (id: string) => string; wallet: (id: string) => string; create: string; migration: string; }; race: { root: string; all: string; detail: (id: string) => string; results: (id: string) => string; stewarding: (id: string) => string; }; team: { root: string; leaderboard: string; detail: (id: string) => string; create: string; }; driver: { root: string; detail: (id: string) => string; }; leaderboards: { root: string; drivers: string; }; error: { notFound: string; serverError: string; }; } /** * Route configuration with i18n support * * Usage: * ```typescript * import { routes } from '@/lib/routing/RouteConfig'; * * // Navigate to login * router.push(routes.auth.login); * * // Navigate to league detail * router.push(routes.league.detail('league-123')); * * // Check if current path is protected * if (currentPath.startsWith(routes.protected.dashboard)) { * // Handle protected route * } * ``` */ export const routes: RouteGroup & { leaderboards: { root: string; drivers: string } } = { auth: { login: '/auth/login', signup: '/auth/signup', forgotPassword: '/auth/forgot-password', resetPassword: '/auth/reset-password', }, public: { home: '/', leagues: '/leagues', drivers: '/drivers', teams: '/teams', leaderboards: '/leaderboards', races: '/races', sponsorSignup: '/sponsor/signup', }, protected: { dashboard: '/dashboard', onboarding: '/onboarding', profile: '/profile', profileSettings: '/profile/settings', profileLeagues: '/profile/leagues', profileLiveries: '/profile/liveries', profileLiveryUpload: '/profile/liveries/upload', profileSponsorshipRequests: '/profile/sponsorship-requests', }, sponsor: { root: '/sponsor', dashboard: '/sponsor/dashboard', billing: '/sponsor/billing', campaigns: '/sponsor/campaigns', leagues: '/sponsor/leagues', leagueDetail: (id: string) => `/sponsor/leagues/${id}`, settings: '/sponsor/settings', }, admin: { root: '/admin', users: '/admin/users', }, league: { detail: (id: string) => `/leagues/${id}`, rosterAdmin: (id: string) => `/leagues/${id}/roster/admin`, rulebook: (id: string) => `/leagues/${id}/rulebook`, schedule: (id: string) => `/leagues/${id}/schedule`, scheduleAdmin: (id: string) => `/leagues/${id}/schedule/admin`, settings: (id: string) => `/leagues/${id}/settings`, sponsorships: (id: string) => `/leagues/${id}/sponsorships`, standings: (id: string) => `/leagues/${id}/standings`, stewarding: (id: string) => `/leagues/${id}/stewarding`, wallet: (id: string) => `/leagues/${id}/wallet`, create: '/leagues/create', migration: '/leagues/migration', }, race: { root: '/races', all: '/races/all', detail: (id: string) => `/races/${id}`, results: (id: string) => `/races/${id}/results`, stewarding: (id: string) => `/races/${id}/stewarding`, }, team: { root: '/teams', leaderboard: '/teams/leaderboard', detail: (id: string) => `/teams/${id}`, create: '/teams/create', }, driver: { root: '/drivers', detail: (id: string) => `/drivers/${id}`, }, leaderboards: { root: '/leaderboards', drivers: '/leaderboards/drivers', }, error: { notFound: '/404', serverError: '/500', }, }; /** * Route matcher utilities for pattern matching */ export const routeMatchers = { /** * Check if path matches a pattern */ matches(path: string, pattern: string): boolean { // Exact match if (pattern === path) return true; // Wildcard match (starts with) if (pattern.endsWith('/*') && path.startsWith(pattern.slice(0, -2))) { return true; } // Parameterized match (e.g., /leagues/[id]) const paramPattern = pattern.replace(/\[([^\]]+)\]/g, '([^/]+)'); const regex = new RegExp(`^${paramPattern}$`); return regex.test(path); }, /** * Check if path is in a route group */ isInGroup(path: string, group: keyof RouteGroup): boolean { const groupRoutes = routes[group]; // Handle nested objects (like sponsor.leagueDetail) const values = Object.values(groupRoutes); return values.some(value => { if (typeof value === 'function') { // For parameterized routes, check pattern const pattern = value('placeholder'); return path.startsWith(pattern.replace('/placeholder', '')); } return path.startsWith(value as string); }); }, /** * Get all public route patterns */ getPublicPatterns(): string[] { return [ routes.public.home, routes.public.leagues, routes.public.drivers, routes.public.teams, routes.public.leaderboards, routes.public.races, routes.public.sponsorSignup, routes.auth.login, routes.auth.signup, routes.auth.forgotPassword, routes.auth.resetPassword, routes.error.notFound, routes.error.serverError, ]; }, /** * Check if path is public */ isPublic(path: string): boolean { // logger.info('[RouteConfig] isPublic check', { path }); const publicPatterns = this.getPublicPatterns(); // Check exact matches if (publicPatterns.includes(path)) { // logger.info('[RouteConfig] Path is public (exact match)', { path }); return true; } // Treat top-level detail pages as public (e2e relies on this) // Examples: /leagues/:id, /races/:id, /drivers/:id, /teams/:id const segments = path.split('/').filter(Boolean); if (segments.length === 2) { const [group, slug] = segments; if (group === 'leagues' && slug !== 'create') { // logger.info('[RouteConfig] Path is public (league detail)', { path }); return true; } if (group === 'races') { // logger.info('[RouteConfig] Path is public (race detail)', { path }); return true; } if (group === 'drivers') { // logger.info('[RouteConfig] Path is public (driver detail)', { path }); return true; } if (group === 'teams' && slug !== 'create') { // logger.info('[RouteConfig] Path is public (team detail)', { path }); return true; } } // Check parameterized patterns const isPublicParam = publicPatterns.some(pattern => { if (pattern.includes('[')) { const paramPattern = pattern.replace(/\[([^\]]+)\]/g, '([^/]+)'); const regex = new RegExp(`^${paramPattern}$`); return regex.test(path); } return false; }); // if (isPublicParam) { // logger.info('[RouteConfig] Path is public (parameterized match)', { path }); // } else { // logger.info('[RouteConfig] Path is NOT public', { path }); // } return isPublicParam; }, /** * Check if path requires authentication */ requiresAuth(path: string): boolean { return !this.isPublic(path); }, /** * Check if path requires specific role */ requiresRole(path: string): string[] | null { // logger.info('[RouteConfig] requiresRole check', { path }); // Public routes never require a role if (this.isPublic(path)) { // logger.info('[RouteConfig] Path is public, no role required', { path }); return null; } if (this.isInGroup(path, 'admin')) { // Website session roles come from the API and are more specific than just "admin". // Keep "admin"/"owner" for backwards compatibility. const roles = ['admin', 'owner', 'league-admin', 'league-steward', 'league-owner', 'system-owner', 'super-admin']; // logger.info('[RouteConfig] Path requires admin roles', { path, roles }); return roles; } if (this.isInGroup(path, 'sponsor')) { // logger.info('[RouteConfig] Path requires sponsor role', { path }); return ['sponsor']; } // logger.info('[RouteConfig] Path requires no specific role', { path }); return null; }, }; /** * i18n-ready path builder * * Usage: * ```typescript * // With current locale * const path = buildPath('leagueDetail', { id: '123' }); * * // With specific locale * const path = buildPath('leagueDetail', { id: '123' }, 'de'); * ``` */ export function buildPath( routeName: string, params: Record = {}, _: string = '' ): string { // This is a placeholder for future i18n implementation // For now, it just builds the path using the route config const parts = routeName.split('.'); let route: any = routes; for (const part of parts) { route = (route as Record)[part]; if (!route) { throw new Error(`Unknown route: ${routeName}`); } } if (typeof route === 'function') { const paramKeys = Object.keys(params); const paramKey = paramKeys[0]; if (!paramKey) { throw new Error(`Route ${routeName} requires parameters`); } return route(params[paramKey]); } return route as string; }