website refactor
This commit is contained in:
@@ -271,7 +271,7 @@ export class LeagueService {
|
||||
throw new Error(err.code);
|
||||
}
|
||||
|
||||
this.allLeaguesWithCapacityPresenter.present(result.unwrap());
|
||||
await this.allLeaguesWithCapacityPresenter.present(result.unwrap());
|
||||
return this.allLeaguesWithCapacityPresenter.getViewModel();
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ export class LeagueService {
|
||||
throw new Error(err.code);
|
||||
}
|
||||
|
||||
this.allLeaguesWithCapacityAndScoringPresenter.present(result.unwrap());
|
||||
await this.allLeaguesWithCapacityAndScoringPresenter.present(result.unwrap());
|
||||
return this.allLeaguesWithCapacityAndScoringPresenter.getViewModel();
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
this.totalLeaguesPresenter.present(result.unwrap());
|
||||
await this.totalLeaguesPresenter.present(result.unwrap());
|
||||
return this.totalLeaguesPresenter.getResponseModel()!;
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ export class LeagueService {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
this.leagueJoinRequestsPresenter.present(result.unwrap());
|
||||
await this.leagueJoinRequestsPresenter.present(result.unwrap());
|
||||
return this.leagueJoinRequestsPresenter.getViewModel()!.joinRequests;
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ export class LeagueService {
|
||||
throw new Error(err.code);
|
||||
}
|
||||
|
||||
this.approveLeagueJoinRequestPresenter.present(result.unwrap());
|
||||
await this.approveLeagueJoinRequestPresenter.present(result.unwrap());
|
||||
return this.approveLeagueJoinRequestPresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -384,7 +384,7 @@ export class LeagueService {
|
||||
throw new Error(err.code);
|
||||
}
|
||||
|
||||
this.rejectLeagueJoinRequestPresenter.present(result.unwrap());
|
||||
await this.rejectLeagueJoinRequestPresenter.present(result.unwrap());
|
||||
return this.rejectLeagueJoinRequestPresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -415,7 +415,7 @@ export class LeagueService {
|
||||
throw new Error(err.code);
|
||||
}
|
||||
|
||||
this.approveLeagueJoinRequestPresenter.present(result.unwrap());
|
||||
await this.approveLeagueJoinRequestPresenter.present(result.unwrap());
|
||||
return this.approveLeagueJoinRequestPresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -432,7 +432,7 @@ export class LeagueService {
|
||||
throw new NotFoundException('Join request not found');
|
||||
}
|
||||
|
||||
this.rejectLeagueJoinRequestPresenter.present(result.unwrap());
|
||||
await this.rejectLeagueJoinRequestPresenter.present(result.unwrap());
|
||||
return this.rejectLeagueJoinRequestPresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -450,7 +450,7 @@ export class LeagueService {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
this.getLeagueAdminPermissionsPresenter.present(result.unwrap());
|
||||
await this.getLeagueAdminPermissionsPresenter.present(result.unwrap());
|
||||
return this.getLeagueAdminPermissionsPresenter.getResponseModel()!;
|
||||
}
|
||||
|
||||
@@ -479,7 +479,7 @@ export class LeagueService {
|
||||
throw new Error(err.code);
|
||||
}
|
||||
|
||||
this.removeLeagueMemberPresenter.present(result.unwrap());
|
||||
await this.removeLeagueMemberPresenter.present(result.unwrap());
|
||||
return this.removeLeagueMemberPresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -517,7 +517,7 @@ export class LeagueService {
|
||||
throw new Error(err.code);
|
||||
}
|
||||
|
||||
this.updateLeagueMemberRolePresenter.present(result.unwrap());
|
||||
await this.updateLeagueMemberRolePresenter.present(result.unwrap());
|
||||
return this.updateLeagueMemberRolePresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -527,7 +527,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
this.getLeagueOwnerSummaryPresenter.present(result.unwrap());
|
||||
await this.getLeagueOwnerSummaryPresenter.present(result.unwrap());
|
||||
return this.getLeagueOwnerSummaryPresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -539,7 +539,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
this.leagueConfigPresenter.present(result.unwrap());
|
||||
await this.leagueConfigPresenter.present(result.unwrap());
|
||||
return this.leagueConfigPresenter.getViewModel();
|
||||
} catch (error) {
|
||||
this.logger.error('Error getting league full config', error instanceof Error ? error : new Error(String(error)));
|
||||
@@ -553,7 +553,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
this.leagueProtestsPresenter.present(result.unwrap());
|
||||
await this.leagueProtestsPresenter.present(result.unwrap());
|
||||
return this.leagueProtestsPresenter.getResponseModel()!;
|
||||
}
|
||||
|
||||
@@ -563,7 +563,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
this.getLeagueSeasonsPresenter.present(result.unwrap());
|
||||
await this.getLeagueSeasonsPresenter.present(result.unwrap());
|
||||
return this.getLeagueSeasonsPresenter.getResponseModel()!;
|
||||
}
|
||||
|
||||
@@ -573,7 +573,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
this.getLeagueMembershipsPresenter.present(result.unwrap());
|
||||
await this.getLeagueMembershipsPresenter.present(result.unwrap());
|
||||
return this.getLeagueMembershipsPresenter.getViewModel()!.memberships;
|
||||
}
|
||||
|
||||
@@ -589,7 +589,7 @@ export class LeagueService {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
this.getLeagueRosterMembersPresenter.present(result.unwrap());
|
||||
await this.getLeagueRosterMembersPresenter.present(result.unwrap());
|
||||
return this.getLeagueRosterMembersPresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -605,7 +605,7 @@ export class LeagueService {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
this.getLeagueRosterJoinRequestsPresenter.present(result.unwrap());
|
||||
await this.getLeagueRosterJoinRequestsPresenter.present(result.unwrap());
|
||||
return this.getLeagueRosterJoinRequestsPresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -615,7 +615,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
this.leagueStandingsPresenter.present(result.unwrap());
|
||||
await this.leagueStandingsPresenter.present(result.unwrap());
|
||||
return this.leagueStandingsPresenter.getResponseModel()!;
|
||||
}
|
||||
|
||||
@@ -629,7 +629,7 @@ export class LeagueService {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
this.leagueSchedulePresenter.present(result.unwrap());
|
||||
await this.leagueSchedulePresenter.present(result.unwrap());
|
||||
return this.leagueSchedulePresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -649,7 +649,7 @@ export class LeagueService {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
this.publishLeagueSeasonSchedulePresenter.present(result.unwrap());
|
||||
await this.publishLeagueSeasonSchedulePresenter.present(result.unwrap());
|
||||
return this.publishLeagueSeasonSchedulePresenter.getResponseModel()!;
|
||||
}
|
||||
|
||||
@@ -669,7 +669,7 @@ export class LeagueService {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
this.unpublishLeagueSeasonSchedulePresenter.present(result.unwrap());
|
||||
await this.unpublishLeagueSeasonSchedulePresenter.present(result.unwrap());
|
||||
return this.unpublishLeagueSeasonSchedulePresenter.getResponseModel()!;
|
||||
}
|
||||
|
||||
@@ -698,7 +698,7 @@ export class LeagueService {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
this.createLeagueSeasonScheduleRacePresenter.present(result.unwrap());
|
||||
await this.createLeagueSeasonScheduleRacePresenter.present(result.unwrap());
|
||||
return this.createLeagueSeasonScheduleRacePresenter.getResponseModel()!;
|
||||
}
|
||||
|
||||
@@ -731,7 +731,7 @@ export class LeagueService {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
this.updateLeagueSeasonScheduleRacePresenter.present(result.unwrap());
|
||||
await this.updateLeagueSeasonScheduleRacePresenter.present(result.unwrap());
|
||||
return this.updateLeagueSeasonScheduleRacePresenter.getResponseModel()!;
|
||||
}
|
||||
|
||||
@@ -749,7 +749,7 @@ export class LeagueService {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
this.deleteLeagueSeasonScheduleRacePresenter.present(result.unwrap());
|
||||
await this.deleteLeagueSeasonScheduleRacePresenter.present(result.unwrap());
|
||||
return this.deleteLeagueSeasonScheduleRacePresenter.getResponseModel()!;
|
||||
}
|
||||
|
||||
@@ -759,7 +759,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
this.leagueStatsPresenter.present(result.unwrap());
|
||||
await this.leagueStatsPresenter.present(result.unwrap());
|
||||
return this.leagueStatsPresenter.getResponseModel()!;
|
||||
}
|
||||
|
||||
@@ -778,13 +778,13 @@ export class LeagueService {
|
||||
}
|
||||
|
||||
// Present the full config result
|
||||
this.leagueConfigPresenter.present(fullConfigResult.unwrap());
|
||||
await this.leagueConfigPresenter.present(fullConfigResult.unwrap());
|
||||
|
||||
const ownerSummaryResult = await this.getLeagueOwnerSummaryUseCase.execute({ leagueId });
|
||||
if (ownerSummaryResult.isErr()) {
|
||||
throw new Error(ownerSummaryResult.unwrapErr().code);
|
||||
}
|
||||
this.getLeagueOwnerSummaryPresenter.present(ownerSummaryResult.unwrap());
|
||||
await this.getLeagueOwnerSummaryPresenter.present(ownerSummaryResult.unwrap());
|
||||
const ownerSummary = this.getLeagueOwnerSummaryPresenter.getViewModel()!;
|
||||
|
||||
const configForm = this.leagueConfigPresenter.getViewModel();
|
||||
@@ -817,7 +817,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
this.createLeaguePresenter.present(result.unwrap());
|
||||
await this.createLeaguePresenter.present(result.unwrap());
|
||||
return this.createLeaguePresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -829,7 +829,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
this.leagueScoringConfigPresenter.present(result.unwrap());
|
||||
await this.leagueScoringConfigPresenter.present(result.unwrap());
|
||||
return this.leagueScoringConfigPresenter.getViewModel();
|
||||
} catch (error) {
|
||||
this.logger.error('Error getting league scoring config', error instanceof Error ? error : new Error(String(error)));
|
||||
@@ -844,7 +844,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
this.leagueScoringPresetsPresenter.present(result.unwrap());
|
||||
await this.leagueScoringPresetsPresenter.present(result.unwrap());
|
||||
return this.leagueScoringPresetsPresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -856,7 +856,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
this.joinLeaguePresenter.present(result.unwrap());
|
||||
await this.joinLeaguePresenter.present(result.unwrap());
|
||||
return this.joinLeaguePresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -877,7 +877,7 @@ export class LeagueService {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
this.transferLeagueOwnershipPresenter.present(result.unwrap());
|
||||
await this.transferLeagueOwnershipPresenter.present(result.unwrap());
|
||||
return this.transferLeagueOwnershipPresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -888,7 +888,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
this.seasonSponsorshipsPresenter.present(result.unwrap());
|
||||
await this.seasonSponsorshipsPresenter.present(result.unwrap());
|
||||
return this.seasonSponsorshipsPresenter.getViewModel()!;
|
||||
}
|
||||
|
||||
@@ -904,7 +904,7 @@ export class LeagueService {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
this.leagueSchedulePresenter.present(result.unwrap());
|
||||
await this.leagueSchedulePresenter.present(result.unwrap());
|
||||
return {
|
||||
races: this.leagueSchedulePresenter.getViewModel()?.races ?? [],
|
||||
};
|
||||
@@ -916,7 +916,7 @@ export class LeagueService {
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
this.getLeagueWalletPresenter.present(result.unwrap());
|
||||
await this.getLeagueWalletPresenter.present(result.unwrap());
|
||||
return this.getLeagueWalletPresenter.getResponseModel();
|
||||
}
|
||||
|
||||
@@ -940,7 +940,7 @@ export class LeagueService {
|
||||
throw new Error(result.unwrapErr().code);
|
||||
}
|
||||
|
||||
this.withdrawFromLeagueWalletPresenter.present(result.unwrap());
|
||||
await this.withdrawFromLeagueWalletPresenter.present(result.unwrap());
|
||||
return this.withdrawFromLeagueWalletPresenter.getResponseModel();
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,17 @@ export class InitializationLogger {
|
||||
}
|
||||
|
||||
log(message: string): void {
|
||||
console.log(`[Initialization] ${message}`);
|
||||
const timestamp = new Date().toISOString();
|
||||
console.log(`[${timestamp}] [Initialization] ${message}`);
|
||||
}
|
||||
|
||||
error(message: string): void {
|
||||
console.error(`[Initialization] ${message}`);
|
||||
const timestamp = new Date().toISOString();
|
||||
console.error(`[${timestamp}] [Initialization] ${message}`);
|
||||
}
|
||||
|
||||
warn(message: string): void {
|
||||
console.warn(`[Initialization] ${message}`);
|
||||
const timestamp = new Date().toISOString();
|
||||
console.warn(`[${timestamp}] [Initialization] ${message}`);
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,20 @@
|
||||
"gridpilot-rules/rsc-no-object-construction": "error",
|
||||
"gridpilot-rules/rsc-no-container-manager-calls": "error",
|
||||
"gridpilot-rules/no-hardcoded-search-params": "error",
|
||||
"gridpilot-rules/no-next-cookies-in-pages": "error"
|
||||
"gridpilot-rules/no-next-cookies-in-pages": "error",
|
||||
"gridpilot-rules/no-hardcoded-routes": "error",
|
||||
"gridpilot-rules/component-classification": "error",
|
||||
"gridpilot-rules/no-raw-html-in-app": "error",
|
||||
"gridpilot-rules/no-console": "error",
|
||||
"import/no-default-export": "off",
|
||||
"no-restricted-syntax": "off",
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react/no-unescaped-entities": "error",
|
||||
"gridpilot-rules/no-index-files": "error",
|
||||
"gridpilot-rules/no-direct-process-env": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -367,4 +380,4 @@
|
||||
"typescript": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,8 +159,12 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
const hasUseClient = sourceCode.getText().includes("'use client'") || sourceCode.getText().includes('"use client"');
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (hasUseClient) return;
|
||||
if (node.callee.type === 'MemberExpression' &&
|
||||
['sort', 'filter', 'reduce'].includes(node.callee.property.name) &&
|
||||
!isInComment(node) &&
|
||||
@@ -273,8 +277,12 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
const hasUseClient = sourceCode.getText().includes("'use client'") || sourceCode.getText().includes('"use client"');
|
||||
|
||||
return {
|
||||
FunctionDeclaration(node) {
|
||||
if (hasUseClient) return;
|
||||
// Skip if this is the main component (default export or ends with Page/Template)
|
||||
const filename = context.getFilename();
|
||||
const isMainComponent =
|
||||
@@ -294,6 +302,7 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
VariableDeclarator(node) {
|
||||
if (hasUseClient) return;
|
||||
// Skip if this is the main component
|
||||
const isMainComponent =
|
||||
(node.parent && node.parent.parent && node.parent.parent.type === 'ExportDefaultDeclaration') ||
|
||||
@@ -329,8 +338,12 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
const hasUseClient = sourceCode.getText().includes("'use client'") || sourceCode.getText().includes('"use client"');
|
||||
|
||||
return {
|
||||
NewExpression(node) {
|
||||
if (hasUseClient) return;
|
||||
if (node.callee.type === 'Identifier' &&
|
||||
/^[A-Z]/.test(node.callee.name) &&
|
||||
!node.callee.name.endsWith('PageQuery') &&
|
||||
|
||||
@@ -16,6 +16,8 @@ import { WalletsApiClient } from './wallets/WalletsApiClient';
|
||||
import { ErrorReporter } from '../interfaces/ErrorReporter';
|
||||
import { Logger } from '../interfaces/Logger';
|
||||
|
||||
import { ConsoleLogger } from '../infrastructure/logging/ConsoleLogger';
|
||||
|
||||
export class ApiClient {
|
||||
public readonly admin: AdminApiClient;
|
||||
public readonly analytics: AnalyticsApiClient;
|
||||
@@ -35,7 +37,7 @@ export class ApiClient {
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
// Default implementations for logger and error reporter if needed
|
||||
const logger: Logger = console;
|
||||
const logger: Logger = new ConsoleLogger();
|
||||
const errorReporter: ErrorReporter = { report: (error) => console.error(error) };
|
||||
|
||||
this.admin = new AdminApiClient(baseUrl, errorReporter, logger);
|
||||
|
||||
@@ -227,8 +227,13 @@ export function handleAuthFlow(
|
||||
|
||||
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 :
|
||||
session?.role === 'admin' ? routes.admin.root :
|
||||
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 };
|
||||
|
||||
@@ -36,6 +36,7 @@ export class ConsoleLogger implements Logger {
|
||||
const color = this.COLORS[level];
|
||||
const emoji = this.EMOJIS[level];
|
||||
const prefix = this.PREFIXES[level];
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Edge runtime doesn't support console.groupCollapsed/groupEnd
|
||||
// Fallback to simple logging for compatibility
|
||||
@@ -44,13 +45,13 @@ export class ConsoleLogger implements Logger {
|
||||
if (supportsGrouping) {
|
||||
// Safe to call - we've verified both functions exist
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(console as any).groupCollapsed(`%c${emoji} [${source.toUpperCase()}] ${prefix}: ${message}`, `color: ${color}; font-weight: bold;`);
|
||||
(console as any).groupCollapsed(`%c${emoji} [${timestamp}] [${source.toUpperCase()}] ${prefix}: ${message}`, `color: ${color}; font-weight: bold;`);
|
||||
} else {
|
||||
// Simple format for edge runtime
|
||||
console.log(`${emoji} [${source.toUpperCase()}] ${prefix}: ${message}`);
|
||||
console.log(`${emoji} [${timestamp}] [${source.toUpperCase()}] ${prefix}: ${message}`);
|
||||
}
|
||||
|
||||
console.log(`%cTimestamp:`, 'color: #666; font-weight: bold;', new Date().toISOString());
|
||||
console.log(`%cTimestamp:`, 'color: #666; font-weight: bold;', timestamp);
|
||||
console.log(`%cSource:`, 'color: #666; font-weight: bold;', source);
|
||||
|
||||
if (context) {
|
||||
|
||||
@@ -366,7 +366,7 @@ export function buildPath(
|
||||
let route: any = routes;
|
||||
|
||||
for (const part of parts) {
|
||||
route = route[part];
|
||||
route = (route as Record<string, any>)[part];
|
||||
if (!route) {
|
||||
throw new Error(`Unknown route: ${routeName}`);
|
||||
}
|
||||
|
||||
@@ -132,12 +132,12 @@ export class LeagueService implements Service {
|
||||
}
|
||||
}
|
||||
|
||||
async getAllLeagues(): Promise<any> {
|
||||
async getAllLeagues(): Promise<Result<AllLeaguesWithCapacityAndScoringDTO, DomainError>> {
|
||||
try {
|
||||
const dto = await this.apiClient.getAllWithCapacityAndScoring();
|
||||
return (dto as any).value || dto;
|
||||
return Result.ok(dto);
|
||||
} catch (error: unknown) {
|
||||
throw error;
|
||||
return Result.err({ type: 'serverError', message: (error as Error).message || 'Failed to fetch leagues' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
const mockGetSession = vi.fn();
|
||||
const mockGetSessionFromRequest = vi.fn();
|
||||
|
||||
// Mock Next.js server components
|
||||
vi.mock('next/server', () => ({
|
||||
@@ -20,125 +20,36 @@ vi.mock('next/server', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock SessionGateway so tests can control session behavior via `mockGetSession`.
|
||||
// Mock SessionGateway
|
||||
vi.mock('@/lib/gateways/SessionGateway', () => ({
|
||||
SessionGateway: class {
|
||||
getSession = mockGetSession;
|
||||
getSessionFromRequest = mockGetSessionFromRequest;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/auth/RouteAccessPolicy', () => ({
|
||||
RouteAccessPolicy: vi.fn().mockImplementation(() => ({
|
||||
isPublic: vi.fn(),
|
||||
isAuthPage: vi.fn(),
|
||||
requiredRoles: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/auth/PathnameInterpreter', () => ({
|
||||
PathnameInterpreter: vi.fn().mockImplementation(() => ({
|
||||
interpret: vi.fn((pathname: string) => ({
|
||||
locale: null,
|
||||
logicalPathname: pathname,
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/auth/AuthRedirectBuilder', () => ({
|
||||
AuthRedirectBuilder: vi.fn().mockImplementation(() => ({
|
||||
toLogin: vi.fn(({ currentPathname }) => `/auth/login?returnTo=${encodeURIComponent(currentPathname)}`),
|
||||
awayFromAuthPage: vi.fn(({ session }) => {
|
||||
const role = session.user?.role;
|
||||
if (role === 'sponsor') return '/sponsor/dashboard';
|
||||
if (role === 'admin' || role === 'league-admin' || role === 'league-steward' || role === 'league-owner' || role === 'system-owner' || role === 'super-admin') return '/admin';
|
||||
return '/dashboard';
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/auth/ReturnToSanitizer', () => ({
|
||||
ReturnToSanitizer: vi.fn().mockImplementation(() => ({
|
||||
sanitizeReturnTo: vi.fn((input, fallback) => input || fallback),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/auth/RoutePathBuilder', () => ({
|
||||
RoutePathBuilder: vi.fn().mockImplementation(() => ({
|
||||
build: vi.fn((routeId, params, options) => {
|
||||
const paths: Record<string, string> = {
|
||||
'auth.login': '/auth/login',
|
||||
'protected.dashboard': '/dashboard',
|
||||
'sponsor.dashboard': '/sponsor/dashboard',
|
||||
'admin': '/admin',
|
||||
};
|
||||
const path = paths[routeId] || '/';
|
||||
return options?.locale ? `/${options.locale}${path}` : path;
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock RouteConfig to have deterministic behavior in tests
|
||||
vi.mock('@/lib/routing/RouteConfig', () => ({
|
||||
routes: {
|
||||
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' },
|
||||
sponsor: { root: '/sponsor', dashboard: '/sponsor/dashboard', billing: '/sponsor/billing' },
|
||||
admin: { root: '/admin', users: '/admin/users' },
|
||||
league: { detail: (id: string) => `/leagues/${id}`, scheduleAdmin: (id: string) => `/leagues/${id}/schedule/admin` },
|
||||
race: { root: '/races', detail: (id: string) => `/races/${id}`, stewarding: (id: string) => `/races/${id}/stewarding` },
|
||||
team: { root: '/teams', detail: (id: string) => `/teams/${id}` },
|
||||
driver: { root: '/drivers', detail: (id: string) => `/drivers/${id}` },
|
||||
error: { notFound: '/404', serverError: '/500' },
|
||||
auth: { login: '/auth/login' },
|
||||
public: { home: '/' },
|
||||
protected: { dashboard: '/dashboard' },
|
||||
sponsor: { root: '/sponsor', dashboard: '/sponsor/dashboard' },
|
||||
admin: { root: '/admin' },
|
||||
},
|
||||
routeMatchers: {
|
||||
isInGroup: (path: string, group: string) => {
|
||||
const groups: Record<string, string[]> = {
|
||||
auth: ['/auth/login', '/auth/signup', '/auth/forgot-password', '/auth/reset-password'],
|
||||
sponsor: ['/sponsor', '/sponsor/dashboard', '/sponsor/billing'],
|
||||
admin: ['/admin', '/admin/users'],
|
||||
};
|
||||
|
||||
return groups[group]?.some((prefix) => path.startsWith(prefix)) ?? false;
|
||||
},
|
||||
isPublic: (path: string) => {
|
||||
const publicExact = new Set([
|
||||
'/',
|
||||
'/leagues',
|
||||
'/drivers',
|
||||
'/teams',
|
||||
'/leaderboards',
|
||||
'/races',
|
||||
'/sponsor/signup',
|
||||
'/auth/login',
|
||||
'/auth/signup',
|
||||
'/auth/forgot-password',
|
||||
'/auth/reset-password',
|
||||
'/404',
|
||||
'/500',
|
||||
]);
|
||||
|
||||
// Check exact matches
|
||||
if (publicExact.has(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') return true;
|
||||
if (group === 'races') return true;
|
||||
if (group === 'drivers') return true;
|
||||
if (group === 'teams') return true;
|
||||
}
|
||||
|
||||
return ['/', '/auth/login'].includes(path);
|
||||
},
|
||||
isInGroup: (path: string, group: string) => {
|
||||
if (group === 'admin') return path.startsWith('/admin');
|
||||
if (group === 'sponsor') return path.startsWith('/sponsor');
|
||||
return false;
|
||||
},
|
||||
requiresAuth: (path: string) => {
|
||||
const publicPaths = ['/', '/leagues', '/drivers', '/teams', '/leaderboards', '/races', '/sponsor/signup', '/auth/login', '/auth/signup', '/auth/forgot-password', '/auth/reset-password', '/404', '/500'];
|
||||
return !publicPaths.includes(path) && !path.startsWith('/leagues/') && !path.startsWith('/drivers/') && !path.startsWith('/teams/') && !path.startsWith('/races/');
|
||||
return !['/', '/auth/login'].includes(path);
|
||||
},
|
||||
requiresRole: (path: string) => {
|
||||
if (path.startsWith('/admin')) return ['league-admin'];
|
||||
if (path.startsWith('/admin')) return ['admin'];
|
||||
if (path.startsWith('/sponsor')) return ['sponsor'];
|
||||
return null;
|
||||
},
|
||||
@@ -153,213 +64,99 @@ describe('Middleware - Route Protection', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockGetSession.mockReset();
|
||||
mockGetSessionFromRequest.mockReset();
|
||||
|
||||
mockRequest = {
|
||||
url: 'http://localhost:3000',
|
||||
nextUrl: { pathname: '/' },
|
||||
method: 'GET',
|
||||
headers: {
|
||||
set: vi.fn(),
|
||||
get: vi.fn(),
|
||||
},
|
||||
cookies: {
|
||||
get: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
set: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
get: vi.fn().mockReturnValue(''),
|
||||
},
|
||||
} as any;
|
||||
});
|
||||
|
||||
describe('x-pathname header', () => {
|
||||
it('should set x-pathname header for all requests', async () => {
|
||||
describe('Redirect logic and returnTo', () => {
|
||||
it('should redirect unauthenticated users to login with returnTo', async () => {
|
||||
mockRequest.nextUrl.pathname = '/dashboard';
|
||||
mockGetSession.mockResolvedValue(null); // No session to trigger redirect
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
// The response should have headers.set called
|
||||
// For redirect responses, check that the mock was set up correctly
|
||||
expect(response.headers).toBeDefined();
|
||||
expect(response.headers.set).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Public routes', () => {
|
||||
it('should allow access to public routes without authentication', async () => {
|
||||
const publicRoutes = ['/', '/leagues', '/drivers', '/teams', '/leaderboards', '/races', '/sponsor/signup'];
|
||||
|
||||
for (const route of publicRoutes) {
|
||||
mockRequest.nextUrl.pathname = route;
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
// Should call NextResponse.next() (no redirect)
|
||||
expect(response).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Protected routes without authentication', () => {
|
||||
it('should redirect to login with returnTo parameter', async () => {
|
||||
mockRequest.nextUrl.pathname = '/dashboard';
|
||||
|
||||
mockGetSession.mockResolvedValue(null);
|
||||
mockGetSessionFromRequest.mockResolvedValue(null);
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
expect(response.url).toContain('/auth/login');
|
||||
expect(response.url).toContain('returnTo=%2Fdashboard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Protected routes with authentication', () => {
|
||||
it('should allow access to protected routes with valid session', async () => {
|
||||
it('should allow authenticated users to access protected routes', async () => {
|
||||
mockRequest.nextUrl.pathname = '/dashboard';
|
||||
mockGetSessionFromRequest.mockResolvedValue({
|
||||
user: { userId: '123', role: 'driver' },
|
||||
});
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
token: 'test-token',
|
||||
user: { userId: '123', role: 'user', email: 'test@example.com', displayName: 'Test User' },
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
// Should not be a redirect (no url property on NextResponse.next() mock)
|
||||
expect(response.url).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Role-based redirects', () => {
|
||||
it('should redirect user with wrong role to their home page', async () => {
|
||||
// Driver trying to access admin
|
||||
mockRequest.nextUrl.pathname = '/admin';
|
||||
mockGetSessionFromRequest.mockResolvedValue({
|
||||
user: { userId: '123', role: 'driver' },
|
||||
});
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
// Should redirect to dashboard (driver's home)
|
||||
expect(response.url).toBe('http://localhost:3000/dashboard');
|
||||
});
|
||||
|
||||
it('should redirect sponsor with wrong role to sponsor dashboard', async () => {
|
||||
// Sponsor trying to access admin
|
||||
mockRequest.nextUrl.pathname = '/admin';
|
||||
mockGetSessionFromRequest.mockResolvedValue({
|
||||
user: { userId: '123', role: 'sponsor' },
|
||||
});
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
expect(response.url).toBe('http://localhost:3000/sponsor/dashboard');
|
||||
});
|
||||
|
||||
it('should allow user with correct role to pass through', async () => {
|
||||
mockRequest.nextUrl.pathname = '/admin';
|
||||
mockGetSessionFromRequest.mockResolvedValue({
|
||||
user: { userId: '123', role: 'admin' },
|
||||
});
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
expect(response.url).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should redirect authenticated users away from auth pages', async () => {
|
||||
mockRequest.nextUrl.pathname = '/auth/login';
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
token: 'test-token',
|
||||
user: { userId: '123', role: 'user', email: 'test@example.com', displayName: 'Test User' },
|
||||
});
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
expect(response.url).toContain('/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Role-based access control', () => {
|
||||
it('should allow admin user to access admin routes', async () => {
|
||||
mockRequest.nextUrl.pathname = '/admin/users';
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
token: 'test-token',
|
||||
user: { userId: '123', role: 'league-admin', email: 'test@example.com', displayName: 'Test User' },
|
||||
});
|
||||
describe('Public routes', () => {
|
||||
it('should allow access to public routes without session', async () => {
|
||||
mockRequest.nextUrl.pathname = '/';
|
||||
mockGetSessionFromRequest.mockResolvedValue(null);
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
expect(response.url).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should block regular user from admin routes', async () => {
|
||||
mockRequest.nextUrl.pathname = '/admin/users';
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
token: 'test-token',
|
||||
user: { userId: '123', role: 'user', email: 'test@example.com', displayName: 'Test User' },
|
||||
});
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
expect(response.url).toContain('/auth/login');
|
||||
});
|
||||
|
||||
it('should allow sponsor user to access sponsor routes', async () => {
|
||||
mockRequest.nextUrl.pathname = '/sponsor/dashboard';
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
token: 'test-token',
|
||||
user: { userId: '123', role: 'sponsor', email: 'test@example.com', displayName: 'Test User' },
|
||||
});
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
expect(response.url).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should block regular user from sponsor routes', async () => {
|
||||
mockRequest.nextUrl.pathname = '/sponsor/dashboard';
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
token: 'test-token',
|
||||
user: { userId: '123', role: 'user', email: 'test@example.com', displayName: 'Test User' },
|
||||
});
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
expect(response.url).toContain('/auth/login');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parameterized routes', () => {
|
||||
it('should allow public access to parameterized public routes', async () => {
|
||||
mockRequest.nextUrl.pathname = '/leagues/123';
|
||||
describe('Special redirects', () => {
|
||||
it('should handle /sponsor root redirect', async () => {
|
||||
mockRequest.nextUrl.pathname = '/sponsor';
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
expect(response).toBeDefined();
|
||||
});
|
||||
|
||||
it('should redirect parameterized protected routes without auth', async () => {
|
||||
mockRequest.nextUrl.pathname = '/leagues/123/schedule/admin';
|
||||
|
||||
mockGetSession.mockResolvedValue(null);
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
expect(response.url).toContain('/auth/login');
|
||||
expect(response.url).toContain('returnTo=%2Fleagues%2F123%2Fschedule%2Fadmin');
|
||||
});
|
||||
|
||||
it('should allow admin access to parameterized admin routes', async () => {
|
||||
mockRequest.nextUrl.pathname = '/leagues/123/schedule/admin';
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
token: 'test-token',
|
||||
user: { userId: '123', role: 'league-admin', email: 'test@example.com', displayName: 'Test User' },
|
||||
});
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
expect(response.url).toBeUndefined();
|
||||
expect(response.url).toBe('http://localhost:3000/sponsor/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle missing session gracefully', async () => {
|
||||
mockRequest.nextUrl.pathname = '/dashboard';
|
||||
|
||||
mockGetSession.mockResolvedValue(null);
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
expect(response.url).toContain('/auth/login');
|
||||
});
|
||||
|
||||
it('should handle session without user role', async () => {
|
||||
mockRequest.nextUrl.pathname = '/admin/users';
|
||||
|
||||
mockGetSession.mockResolvedValue({
|
||||
token: 'test-token',
|
||||
user: { userId: '123', email: 'test@example.com', displayName: 'Test User' }, // no role
|
||||
});
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
expect(response.url).toContain('/auth/login');
|
||||
});
|
||||
|
||||
it('should preserve locale in redirects', async () => {
|
||||
mockRequest.nextUrl.pathname = '/de/dashboard';
|
||||
|
||||
mockGetSession.mockResolvedValue(null);
|
||||
|
||||
const response = await middleware(mockRequest);
|
||||
|
||||
expect(response.url).toContain('/de/auth/login');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user