diff --git a/core/automation/infrastructure/config/AutomationConfig.ts b/adapters/automation/config/AutomationConfig.ts similarity index 100% rename from core/automation/infrastructure/config/AutomationConfig.ts rename to adapters/automation/config/AutomationConfig.ts diff --git a/core/automation/infrastructure/config/BrowserModeConfig.ts b/adapters/automation/config/BrowserModeConfig.ts similarity index 100% rename from core/automation/infrastructure/config/BrowserModeConfig.ts rename to adapters/automation/config/BrowserModeConfig.ts diff --git a/core/automation/infrastructure/config/LoggingConfig.ts b/adapters/automation/config/LoggingConfig.ts similarity index 100% rename from core/automation/infrastructure/config/LoggingConfig.ts rename to adapters/automation/config/LoggingConfig.ts diff --git a/core/automation/infrastructure/config/index.ts b/adapters/automation/config/index.ts similarity index 100% rename from core/automation/infrastructure/config/index.ts rename to adapters/automation/config/index.ts diff --git a/adapters/bootstrap/EnsureInitialData.ts b/adapters/bootstrap/EnsureInitialData.ts new file mode 100644 index 000000000..4e3a01918 --- /dev/null +++ b/adapters/bootstrap/EnsureInitialData.ts @@ -0,0 +1,34 @@ +import { SignupWithEmailUseCase } from '../../core/identity/application/use-cases/SignupWithEmailUseCase'; + +/** + * EnsureInitialData - Bootstrap script to ensure initial data exists. + * Idempotent: Can be run multiple times without issues. + * Calls core use cases to create initial admin user if not exists. + */ +export class EnsureInitialData { + constructor( + private readonly signupUseCase: SignupWithEmailUseCase, + ) {} + + async execute(): Promise { + // Ensure initial admin user exists + try { + await this.signupUseCase.execute({ + email: 'admin@gridpilot.local', + password: 'admin123', + displayName: 'Admin', + }); + // User created successfully + } catch (error) { + if (error instanceof Error && error.message === 'An account with this email already exists') { + // User already exists, nothing to do + return; + } + // Re-throw other errors + throw error; + } + + // Future: Add more initial data creation here + // e.g., create default league, config, etc. + } +} \ No newline at end of file diff --git a/core/identity/infrastructure/session/CookieIdentitySessionAdapter.ts b/adapters/identity/session/CookieIdentitySessionAdapter.ts similarity index 100% rename from core/identity/infrastructure/session/CookieIdentitySessionAdapter.ts rename to adapters/identity/session/CookieIdentitySessionAdapter.ts diff --git a/core/shared/logging/ConsoleLogger.ts b/adapters/logging/ConsoleLogger.ts similarity index 100% rename from core/shared/logging/ConsoleLogger.ts rename to adapters/logging/ConsoleLogger.ts diff --git a/core/notifications/infrastructure/adapters/DiscordNotificationAdapter.ts b/adapters/notifications/adapters/DiscordNotificationAdapter.ts similarity index 100% rename from core/notifications/infrastructure/adapters/DiscordNotificationAdapter.ts rename to adapters/notifications/adapters/DiscordNotificationAdapter.ts diff --git a/core/notifications/infrastructure/adapters/EmailNotificationAdapter.ts b/adapters/notifications/adapters/EmailNotificationAdapter.ts similarity index 100% rename from core/notifications/infrastructure/adapters/EmailNotificationAdapter.ts rename to adapters/notifications/adapters/EmailNotificationAdapter.ts diff --git a/core/notifications/infrastructure/adapters/InAppNotificationAdapter.ts b/adapters/notifications/adapters/InAppNotificationAdapter.ts similarity index 100% rename from core/notifications/infrastructure/adapters/InAppNotificationAdapter.ts rename to adapters/notifications/adapters/InAppNotificationAdapter.ts diff --git a/core/notifications/infrastructure/adapters/NotificationGatewayRegistry.ts b/adapters/notifications/adapters/NotificationGatewayRegistry.ts similarity index 100% rename from core/notifications/infrastructure/adapters/NotificationGatewayRegistry.ts rename to adapters/notifications/adapters/NotificationGatewayRegistry.ts diff --git a/core/analytics/infrastructure/repositories/InMemoryAnalyticsSnapshotRepository.ts b/adapters/persistence/inmemory/analytics/InMemoryAnalyticsSnapshotRepository.ts similarity index 100% rename from core/analytics/infrastructure/repositories/InMemoryAnalyticsSnapshotRepository.ts rename to adapters/persistence/inmemory/analytics/InMemoryAnalyticsSnapshotRepository.ts diff --git a/core/analytics/infrastructure/repositories/InMemoryEngagementRepository.ts b/adapters/persistence/inmemory/analytics/InMemoryEngagementRepository.ts similarity index 100% rename from core/analytics/infrastructure/repositories/InMemoryEngagementRepository.ts rename to adapters/persistence/inmemory/analytics/InMemoryEngagementRepository.ts diff --git a/core/analytics/infrastructure/repositories/InMemoryPageViewRepository.ts b/adapters/persistence/inmemory/analytics/InMemoryPageViewRepository.ts similarity index 100% rename from core/analytics/infrastructure/repositories/InMemoryPageViewRepository.ts rename to adapters/persistence/inmemory/analytics/InMemoryPageViewRepository.ts diff --git a/core/identity/infrastructure/repositories/InMemoryAchievementRepository.ts b/adapters/persistence/inmemory/identity/InMemoryAchievementRepository.ts similarity index 100% rename from core/identity/infrastructure/repositories/InMemoryAchievementRepository.ts rename to adapters/persistence/inmemory/identity/InMemoryAchievementRepository.ts diff --git a/core/identity/infrastructure/repositories/InMemorySponsorAccountRepository.ts b/adapters/persistence/inmemory/identity/InMemorySponsorAccountRepository.ts similarity index 100% rename from core/identity/infrastructure/repositories/InMemorySponsorAccountRepository.ts rename to adapters/persistence/inmemory/identity/InMemorySponsorAccountRepository.ts diff --git a/core/identity/infrastructure/repositories/InMemoryUserRatingRepository.ts b/adapters/persistence/inmemory/identity/InMemoryUserRatingRepository.ts similarity index 100% rename from core/identity/infrastructure/repositories/InMemoryUserRatingRepository.ts rename to adapters/persistence/inmemory/identity/InMemoryUserRatingRepository.ts diff --git a/core/identity/infrastructure/repositories/InMemoryUserRepository.ts b/adapters/persistence/inmemory/identity/InMemoryUserRepository.ts similarity index 100% rename from core/identity/infrastructure/repositories/InMemoryUserRepository.ts rename to adapters/persistence/inmemory/identity/InMemoryUserRepository.ts diff --git a/core/testing-support/src/media/InMemoryAvatarGenerationRepository.ts b/adapters/persistence/inmemory/media/InMemoryAvatarGenerationRepository.ts similarity index 98% rename from core/testing-support/src/media/InMemoryAvatarGenerationRepository.ts rename to adapters/persistence/inmemory/media/InMemoryAvatarGenerationRepository.ts index 7086e348b..372797483 100644 --- a/core/testing-support/src/media/InMemoryAvatarGenerationRepository.ts +++ b/adapters/persistence/inmemory/media/InMemoryAvatarGenerationRepository.ts @@ -41,7 +41,7 @@ export class InMemoryAvatarGenerationRepository implements IAvatarGenerationRepo async findByUserId(userId: string): Promise { this.logger.debug(`Finding avatar generation requests by user ID: ${userId}`); const results: AvatarGenerationRequest[] = []; - for (const props of this.requests.values()) { + for (const props of Array.from(this.requests.values())) { if (props.userId === userId) { results.push(AvatarGenerationRequest.reconstitute(props)); } diff --git a/core/notifications/infrastructure/repositories/InMemoryNotificationPreferenceRepository.ts b/adapters/persistence/inmemory/notifications/InMemoryNotificationPreferenceRepository.ts similarity index 100% rename from core/notifications/infrastructure/repositories/InMemoryNotificationPreferenceRepository.ts rename to adapters/persistence/inmemory/notifications/InMemoryNotificationPreferenceRepository.ts diff --git a/core/notifications/infrastructure/repositories/InMemoryNotificationRepository.ts b/adapters/persistence/inmemory/notifications/InMemoryNotificationRepository.ts similarity index 100% rename from core/notifications/infrastructure/repositories/InMemoryNotificationRepository.ts rename to adapters/persistence/inmemory/notifications/InMemoryNotificationRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryCarRepository.ts b/adapters/persistence/inmemory/racing/InMemoryCarRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryCarRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryCarRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryDriverRepository.ts b/adapters/persistence/inmemory/racing/InMemoryDriverRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryDriverRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryDriverRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryGameRepository.ts b/adapters/persistence/inmemory/racing/InMemoryGameRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryGameRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryGameRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryLeagueMembershipRepository.ts b/adapters/persistence/inmemory/racing/InMemoryLeagueMembershipRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryLeagueMembershipRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryLeagueMembershipRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryLeagueRepository.ts b/adapters/persistence/inmemory/racing/InMemoryLeagueRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryLeagueRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryLeagueRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryLeagueScoringPresetProvider.ts b/adapters/persistence/inmemory/racing/InMemoryLeagueScoringPresetProvider.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryLeagueScoringPresetProvider.ts rename to adapters/persistence/inmemory/racing/InMemoryLeagueScoringPresetProvider.ts diff --git a/core/racing/infrastructure/repositories/InMemoryLeagueWalletRepository.ts b/adapters/persistence/inmemory/racing/InMemoryLeagueWalletRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryLeagueWalletRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryLeagueWalletRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryLiveryRepository.ts b/adapters/persistence/inmemory/racing/InMemoryLiveryRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryLiveryRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryLiveryRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryPenaltyRepository.ts b/adapters/persistence/inmemory/racing/InMemoryPenaltyRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryPenaltyRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryPenaltyRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryProtestRepository.ts b/adapters/persistence/inmemory/racing/InMemoryProtestRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryProtestRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryProtestRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryRaceEventRepository.ts b/adapters/persistence/inmemory/racing/InMemoryRaceEventRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryRaceEventRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryRaceEventRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryRaceRegistrationRepository.ts b/adapters/persistence/inmemory/racing/InMemoryRaceRegistrationRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryRaceRegistrationRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryRaceRegistrationRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryRaceRepository.ts b/adapters/persistence/inmemory/racing/InMemoryRaceRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryRaceRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryRaceRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryResultRepository.ts b/adapters/persistence/inmemory/racing/InMemoryResultRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryResultRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryResultRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryScoringRepositories.ts b/adapters/persistence/inmemory/racing/InMemoryScoringRepositories.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryScoringRepositories.ts rename to adapters/persistence/inmemory/racing/InMemoryScoringRepositories.ts diff --git a/core/racing/infrastructure/repositories/InMemorySeasonSponsorshipRepository.ts b/adapters/persistence/inmemory/racing/InMemorySeasonSponsorshipRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemorySeasonSponsorshipRepository.ts rename to adapters/persistence/inmemory/racing/InMemorySeasonSponsorshipRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemorySessionRepository.ts b/adapters/persistence/inmemory/racing/InMemorySessionRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemorySessionRepository.ts rename to adapters/persistence/inmemory/racing/InMemorySessionRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemorySponsorRepository.ts b/adapters/persistence/inmemory/racing/InMemorySponsorRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemorySponsorRepository.ts rename to adapters/persistence/inmemory/racing/InMemorySponsorRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemorySponsorshipPricingRepository.ts b/adapters/persistence/inmemory/racing/InMemorySponsorshipPricingRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemorySponsorshipPricingRepository.ts rename to adapters/persistence/inmemory/racing/InMemorySponsorshipPricingRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemorySponsorshipRequestRepository.ts b/adapters/persistence/inmemory/racing/InMemorySponsorshipRequestRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemorySponsorshipRequestRepository.ts rename to adapters/persistence/inmemory/racing/InMemorySponsorshipRequestRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryStandingRepository.ts b/adapters/persistence/inmemory/racing/InMemoryStandingRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryStandingRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryStandingRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryTeamMembershipRepository.ts b/adapters/persistence/inmemory/racing/InMemoryTeamMembershipRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryTeamMembershipRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryTeamMembershipRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryTeamRepository.ts b/adapters/persistence/inmemory/racing/InMemoryTeamRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryTeamRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryTeamRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryTrackRepository.ts b/adapters/persistence/inmemory/racing/InMemoryTrackRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryTrackRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryTrackRepository.ts diff --git a/core/racing/infrastructure/repositories/InMemoryTransactionRepository.ts b/adapters/persistence/inmemory/racing/InMemoryTransactionRepository.ts similarity index 100% rename from core/racing/infrastructure/repositories/InMemoryTransactionRepository.ts rename to adapters/persistence/inmemory/racing/InMemoryTransactionRepository.ts diff --git a/adapters/persistence/migrations/001_initial_schema.ts b/adapters/persistence/migrations/001_initial_schema.ts new file mode 100644 index 000000000..c204eab90 --- /dev/null +++ b/adapters/persistence/migrations/001_initial_schema.ts @@ -0,0 +1,12 @@ +// Migration 001: Initial schema setup +// This migration sets up the initial database schema for the application. +// Idempotent: Can be run multiple times without issues. + +export async function up(): Promise { + // For in-memory persistence, no schema changes are needed. + // This migration is a placeholder for future database migrations. +} + +export async function down(): Promise { + // Rollback not implemented for in-memory persistence. +} \ No newline at end of file diff --git a/apps/api/src/application/analytics.module.ts b/apps/api/src/application/analytics.module.ts new file mode 100644 index 000000000..38fa168df --- /dev/null +++ b/apps/api/src/application/analytics.module.ts @@ -0,0 +1,50 @@ +import { Module } from '@nestjs/common'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; + +const ILogger_TOKEN = 'ILogger_TOKEN'; +const IPAGE_VIEW_REPO_TOKEN = 'IPageViewRepository_TOKEN'; +const IENGAGEMENT_REPO_TOKEN = 'IEngagementRepository_TOKEN'; + +import { ILogger } from '@gridpilot/shared/logging/ILogger'; +import { IPageViewRepository } from '@gridpilot/analytics/application/repositories/IPageViewRepository'; +import { IEngagementRepository } from '@gridpilot/analytics/domain/repositories/IEngagementRepository'; + +import { RecordPageViewUseCase } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase'; +import { RecordEngagementUseCase } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase'; + +import { InMemoryPageViewRepository } from '../../../../adapters/persistence/inmemory/analytics/InMemoryPageViewRepository'; +import { TypeOrmEngagementRepository } from '../../../../adapters/persistence/typeorm/analytics/TypeOrmEngagementRepository'; +import { ConsoleLogger } from '../../../../adapters/logging/ConsoleLogger'; +import { AnalyticsController } from '../../presentation/analytics.controller'; + +@Module({ + imports: [], + controllers: [AnalyticsController], + providers: [ + { + provide: ILogger_TOKEN, + useClass: ConsoleLogger, + }, + { + provide: IPAGE_VIEW_REPO_TOKEN, + useClass: InMemoryPageViewRepository, + }, + { + provide: IENGAGEMENT_REPO_TOKEN, + useFactory: (dataSource: DataSource) => new TypeOrmEngagementRepository(dataSource.manager), + inject: [getDataSourceToken()], + }, + { + provide: RecordPageViewUseCase, + useFactory: (repo: IPageViewRepository, logger: ILogger) => new RecordPageViewUseCase(repo, logger), + inject: [IPAGE_VIEW_REPO_TOKEN, ILogger_TOKEN], + }, + { + provide: RecordEngagementUseCase, + useFactory: (repo: IEngagementRepository, logger: ILogger) => new RecordEngagementUseCase(repo, logger), + inject: [IENGAGEMENT_REPO_TOKEN, ILogger_TOKEN], + }, + ], +}) +export class AnalyticsModule {} \ No newline at end of file diff --git a/apps/api/src/infrastructure/database/database.module.ts b/apps/api/src/infrastructure/database/database.module.ts index f6fd50daa..d2ff03bec 100644 --- a/apps/api/src/infrastructure/database/database.module.ts +++ b/apps/api/src/infrastructure/database/database.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { PageViewEntity } from '../analytics/typeorm-page-view.entity'; +import { AnalyticsSnapshotOrmEntity } from '../../../../../adapters/persistence/typeorm/analytics/AnalyticsSnapshotOrmEntity'; +import { EngagementOrmEntity } from '../../../../../adapters/persistence/typeorm/analytics/EngagementOrmEntity'; @Module({ imports: [ @@ -11,9 +12,10 @@ import { PageViewEntity } from '../analytics/typeorm-page-view.entity'; username: process.env.DATABASE_USER || 'user', password: process.env.DATABASE_PASSWORD || 'password', database: process.env.DATABASE_NAME || 'gridpilot', - entities: [PageViewEntity], + entities: [AnalyticsSnapshotOrmEntity, EngagementOrmEntity], synchronize: true, // Use carefully in production }), ], + exports: [TypeOrmModule], }) export class DatabaseModule {} diff --git a/apps/api/src/presentation/analytics.controller.ts b/apps/api/src/presentation/analytics.controller.ts index 720f82e2c..bdde8f38b 100644 --- a/apps/api/src/presentation/analytics.controller.ts +++ b/apps/api/src/presentation/analytics.controller.ts @@ -1,20 +1,25 @@ import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common'; -import { AnalyticsService } from '../application/analytics/analytics.service'; -import { RecordPageViewInput } from '../application/analytics/record-page-view.use-case'; -import { RecordEngagementInput, RecordEngagementOutput } from '../application/analytics/record-engagement.use-case'; + +import type { RecordPageViewInput, RecordPageViewOutput } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase'; +import type { RecordEngagementInput, RecordEngagementOutput } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase'; +import { RecordPageViewUseCase } from '@gridpilot/analytics/application/use-cases/RecordPageViewUseCase'; +import { RecordEngagementUseCase } from '@gridpilot/analytics/application/use-cases/RecordEngagementUseCase'; import { Response } from 'express'; @Controller('analytics') export class AnalyticsController { - constructor(private readonly analyticsService: AnalyticsService) {} + constructor( + private readonly recordPageViewUseCase: RecordPageViewUseCase, + private readonly recordEngagementUseCase: RecordEngagementUseCase, + ) {} @Post('page-view') async recordPageView( @Body() input: RecordPageViewInput, @Res() res: Response, ): Promise { - const { pageViewId } = await this.analyticsService.recordPageView(input); - res.status(HttpStatus.CREATED).json({ pageViewId }); + const output: RecordPageViewOutput = await this.recordPageViewUseCase.execute(input); + res.status(HttpStatus.CREATED).json(output); } @Post('engagement') @@ -22,7 +27,7 @@ export class AnalyticsController { @Body() input: RecordEngagementInput, @Res() res: Response, ): Promise { - const output: RecordEngagementOutput = await this.analyticsService.recordEngagement(input); + const output: RecordEngagementOutput = await this.recordEngagementUseCase.execute(input); res.status(HttpStatus.CREATED).json(output); } } diff --git a/apps/website/lib/auth/InMemoryAuthService.ts b/apps/website/lib/auth/InMemoryAuthService.ts deleted file mode 100644 index d41f11ebf..000000000 --- a/apps/website/lib/auth/InMemoryAuthService.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { AuthService, AuthSession, SignupParams, LoginParams } from './AuthService'; -import type { AuthCallbackCommandDTO } from '@gridpilot/identity/application/dto/AuthCallbackCommandDTO'; -import type { StartAuthCommandDTO } from '@gridpilot/identity/application/dto/StartAuthCommandDTO'; -import { StartAuthUseCase } from '@gridpilot/identity/application/use-cases/StartAuthUseCase'; -import { GetCurrentUserSessionUseCase } from '@gridpilot/identity/application/use-cases/GetCurrentUserSessionUseCase'; -import { HandleAuthCallbackUseCase } from '@gridpilot/identity/application/use-cases/HandleAuthCallbackUseCase'; -import { LogoutUseCase } from '@gridpilot/identity/application/use-cases/LogoutUseCase'; -import { SignupWithEmailUseCase } from '@gridpilot/identity/application/use-cases/SignupWithEmailUseCase'; -import { LoginWithEmailUseCase } from '@gridpilot/identity/application/use-cases/LoginWithEmailUseCase'; -import { CookieIdentitySessionAdapter } from '@gridpilot/identity/infrastructure/session/CookieIdentitySessionAdapter'; -import { IracingDemoIdentityProviderAdapter } from '@gridpilot/identity/infrastructure/providers/IracingDemoIdentityProviderAdapter'; -import { InMemoryUserRepository } from '@gridpilot/identity/infrastructure/repositories/InMemoryUserRepository'; -import type { IUserRepository } from '@gridpilot/identity/domain/repositories/IUserRepository'; -import type { ILogger } from '@gridpilot/shared/logging/ILogger'; - -// Singleton user repository to persist across requests (in-memory demo) -let userRepositoryInstance: IUserRepository | null = null; - -function getUserRepository(logger: ILogger): IUserRepository { - if (!userRepositoryInstance) { - userRepositoryInstance = new InMemoryUserRepository(logger); - } - return userRepositoryInstance; -} - -export class InMemoryAuthService implements AuthService { - private readonly logger: ILogger; - - constructor(logger: ILogger) { - this.logger = logger; - } - - async getCurrentSession(): Promise { - const sessionPort = new CookieIdentitySessionAdapter(); - const useCase = new GetCurrentUserSessionUseCase(sessionPort); - return useCase.execute(); - } - - async signupWithEmail(params: SignupParams): Promise { - const userRepository = getUserRepository(); - const sessionPort = new CookieIdentitySessionAdapter(); - const useCase = new SignupWithEmailUseCase(userRepository, sessionPort); - - const result = await useCase.execute({ - email: params.email, - password: params.password, - displayName: params.displayName, - }); - - return result.session; - } - - async loginWithEmail(params: LoginParams): Promise { - const userRepository = getUserRepository(); - const sessionPort = new CookieIdentitySessionAdapter(); - const useCase = new LoginWithEmailUseCase(userRepository, sessionPort); - - return useCase.execute({ - email: params.email, - password: params.password, - }); - } - - async startIracingAuthRedirect( - returnTo?: string, - ): Promise<{ redirectUrl: string; state: string }> { - const provider = new IracingDemoIdentityProviderAdapter(); - const useCase = new StartAuthUseCase(provider); - - const command: StartAuthCommandDTO = returnTo - ? { - provider: 'IRACING_DEMO', - returnTo, - } - : { - provider: 'IRACING_DEMO', - }; - - return useCase.execute(command); - } - - async loginWithIracingCallback(params: { - code: string; - state: string; - returnTo?: string; - }): Promise { - const provider = new IracingDemoIdentityProviderAdapter(); - const sessionPort = new CookieIdentitySessionAdapter(); - const useCase = new HandleAuthCallbackUseCase(provider, sessionPort); - - const command: AuthCallbackCommandDTO = params.returnTo - ? { - provider: 'IRACING_DEMO', - code: params.code, - state: params.state, - returnTo: params.returnTo, - } - : { - provider: 'IRACING_DEMO', - code: params.code, - state: params.state, - }; - - return useCase.execute(command); - } - - async logout(): Promise { - const sessionPort = new CookieIdentitySessionAdapter(); - const useCase = new LogoutUseCase(sessionPort); - await useCase.execute(); - } -} \ No newline at end of file diff --git a/apps/website/lib/di-config.ts b/apps/website/lib/di-config.ts index 3086e395a..db9a2cf45 100644 --- a/apps/website/lib/di-config.ts +++ b/apps/website/lib/di-config.ts @@ -37,33 +37,48 @@ import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repos import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository'; import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository'; import type { ImageServicePort } from '@gridpilot/media'; -import type { ILogger } from '@gridpilot/shared/logging/ILogger'; -import { ConsoleLogger } from '@gridpilot/shared/logging/ConsoleLogger'; +import type { ILogger } from '@gridpilot/shared/logging'; +import { ConsoleLogger } from '../../../adapters/logging'; import type { IPageViewRepository, IEngagementRepository } from '@gridpilot/analytics'; -import { InMemoryPageViewRepository, InMemoryEngagementRepository } from '@gridpilot/analytics/infrastructure/repositories'; -import { RecordPageViewUseCase, RecordEngagementUseCase } from '@gridpilot/analytics/application/use-cases'; -import type { AuthService } from './auth/AuthService'; -import { InMemoryAuthService } from './auth/InMemoryAuthService'; -import type { IUserRepository, StoredUser } from '@gridpilot/identity/domain/repositories/IUserRepository'; -import { InMemoryUserRepository } from '@gridpilot/identity/infrastructure/repositories/InMemoryUserRepository'; -import type { ISponsorAccountRepository, SponsorAccount } from '@gridpilot/identity/domain/repositories/ISponsorAccountRepository'; -import { InMemorySponsorAccountRepository } from '@gridpilot/identity/infrastructure/repositories/InMemorySponsorAccountRepository'; -import type { ILiveryRepository } from '@gridpilot/racing/domain/repositories/ILiveryRepository'; -import { InMemoryLiveryRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryLiveryRepository'; -import type { IChampionshipStandingRepository } from '@gridpilot/racing/domain/repositories/IChampionshipStandingRepository'; -import { InMemoryChampionshipStandingRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryScoringRepositories'; -import type { ILeagueWalletRepository } from '@gridpilot/racing/domain/repositories/ILeagueWalletRepository'; -import { InMemoryLeagueWalletRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryLeagueWalletRepository'; -import type { ITransactionRepository } from '@gridpilot/racing/domain/repositories/ITransactionRepository'; -import { InMemoryTransactionRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryTransactionRepository'; -import type { ISessionRepository } from '@gridpilot/racing/domain/repositories/ISessionRepository'; -import { InMemorySessionRepository } from '@gridpilot/racing/infrastructure/repositories/InMemorySessionRepository'; -import type { IAchievementRepository } from '@gridpilot/identity/domain/repositories/IAchievementRepository'; -import { InMemoryAchievementRepository } from '@gridpilot/identity/infrastructure/repositories/InMemoryAchievementRepository'; -import type { IUserRatingRepository } from '@gridpilot/identity/domain/repositories/IUserRatingRepository'; -import { InMemoryUserRatingRepository } from '@gridpilot/identity/infrastructure/repositories/InMemoryUserRatingRepository'; +import { ConsoleLogger } from '../../../adapters/logging'; + +import { DI_TOKENS } from './di-tokens'; +// Identity authentication use cases and ports +import { StartAuthUseCase } from '@gridpilot/identity/application/use-cases/StartAuthUseCase'; +import { GetCurrentUserSessionUseCase } from '@gridpilot/identity/application/use-cases/GetCurrentUserSessionUseCase'; +import { HandleAuthCallbackUseCase } from '@gridpilot/identity/application/use-cases/HandleAuthCallbackUseCase'; +import { LogoutUseCase } from '@gridpilot/identity/application/use-cases/LogoutUseCase'; +import { SignupWithEmailUseCase } from '@gridpilot/identity/application/use-cases/SignupWithEmailUseCase'; +import { LoginWithEmailUseCase } from '@gridpilot/identity/application/use-cases/LoginWithEmailUseCase'; +import type { IdentityProvider } from '@gridpilot/identity/application/ports/IdentityProvider'; +import type { SessionPort } from '@gridpilot/identity/application/ports/SessionPort'; +import { IracingDemoIdentityProviderAdapter } from '../../../testing/fakes/identity/IracingDemoIdentityProviderAdapter'; +import { CookieIdentitySessionAdapter } from '@gridpilot/identity/infrastructure/session/CookieIdentitySessionAdapter'; + +import type { AuthService } from './auth/AuthService'; +import type { ILogger } from '@gridpilot/shared/logging'; + +// Repositories +import type { IAchievementRepository } from '@gridpilot/identity/domain/repositories/IAchievementRepository'; +import { InMemoryAchievementRepository } from '../../adapters/persistence/inmemory/identity/InMemoryAchievementRepository'; +import type { IUserRatingRepository } from '@gridpilot/identity/domain/repositories/IUserRatingRepository'; +import { InMemoryUserRatingRepository } from '../../adapters/persistence/inmemory/identity/InMemoryUserRatingRepository'; +import type { ISponsorAccountRepository, SponsorAccount } from '@gridpilot/identity/domain/repositories/ISponsorAccountRepository'; +import { InMemorySponsorAccountRepository } from '../../adapters/persistence/inmemory/identity/InMemorySponsorAccountRepository'; +import type { IUserRepository, StoredUser } from '@gridpilot/identity/domain/repositories/IUserRepository'; +import { InMemoryUserRepository } from '../../adapters/persistence/inmemory/identity/InMemoryUserRepository'; + +import type { ILiveryRepository } from '@gridpilot/racing/domain/repositories/ILiveryRepository'; +import { InMemoryLiveryRepository } from '../../adapters/persistence/inmemory/racing/InMemoryLiveryRepository'; +import type { IChampionshipStandingRepository } from '@gridpilot/racing/domain/repositories/IChampionshipStandingRepository'; +import { InMemoryChampionshipStandingRepository } from '../../adapters/persistence/inmemory/racing/InMemoryScoringRepositories'; +import type { ILeagueWalletRepository } from '@gridpilot/racing/domain/repositories/ILeagueWalletRepository'; +import { InMemoryLeagueWalletRepository } from '../../adapters/persistence/inmemory/racing/InMemoryLeagueWalletRepository'; +import type { ITransactionRepository } from '@gridpilot/racing/domain/repositories/ITransactionRepository'; +import { InMemoryTransactionRepository } from '../../adapters/persistence/inmemory/racing/InMemoryTransactionRepository'; +import type { ISessionRepository } from '@gridpilot/racing/domain/repositories/ISessionRepository'; +import { InMemorySessionRepository } from '../../adapters/persistence/inmemory/racing/InMemorySessionRepository'; -// Notifications import type { INotificationRepository, INotificationPreferenceRepository } from '@gridpilot/notifications/application'; import { SendNotificationUseCase, @@ -77,10 +92,17 @@ import { InAppNotificationAdapter, } from '@gridpilot/notifications/infrastructure'; +import type { IPageViewRepository, IEngagementRepository } from '@gridpilot/analytics'; +import { InMemoryPageViewRepository, InMemoryEngagementRepository } from '../../adapters/persistence/inmemory/analytics/InMemoryAnalyticsRepositories'; +import { + RecordPageViewUseCase, + RecordEngagementUseCase +} from '@gridpilot/analytics/application/use-cases'; + // Infrastructure repositories -import { InMemoryDriverRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryDriverRepository'; -import { InMemoryLeagueRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryLeagueRepository'; -import { InMemoryRaceRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryRaceRepository'; +import { InMemoryDriverRepository } from '../../adapters/persistence/inmemory/racing/InMemoryDriverRepository'; +import { InMemoryLeagueRepository } from '../../adapters/persistence/inmemory/racing/InMemoryLeagueRepository'; +import { InMemoryRaceRepository } from '../../adapters/persistence/inmemory/racing/InMemoryRaceRepository'; import { InMemoryResultRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryResultRepository'; import { InMemoryStandingRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryStandingRepository'; import { InMemoryPenaltyRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryPenaltyRepository'; @@ -180,6 +202,7 @@ import { LeagueStatsPresenter } from './presenters/LeagueStatsPresenter'; import { LeagueScoringConfigPresenter } from './presenters/LeagueScoringConfigPresenter'; import { LeagueFullConfigPresenter } from './presenters/LeagueFullConfigPresenter'; import { LeagueDriverSeasonStatsPresenter } from './presenters/LeagueDriverSeasonStatsPresenter'; +import { LeagueStandingsPresenter as ILeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter'; import { LeagueStandingsPresenter } from './presenters/LeagueStandingsPresenter'; import { LeagueScoringPresetsPresenter } from './presenters/LeagueScoringPresetsPresenter'; import { RaceWithSOFPresenter } from './presenters/RaceWithSOFPresenter'; @@ -245,6 +268,10 @@ export function configureDIContainer(): void { const driverStats: DemoDriverStatsMap = createDemoDriverStats(seedData.drivers); // Register repositories + container.registerInstance( + DI_TOKENS.LeagueStandingsRepository, + new LeagueStandingsRepositoryAdapter() + ); container.registerInstance( DI_TOKENS.DriverRepository, new InMemoryDriverRepository(logger, seedData.drivers) @@ -920,6 +947,7 @@ export function configureDIContainer(): void { const raceRegistrationRepository = container.resolve(DI_TOKENS.RaceRegistrationRepository); const leagueMembershipRepository = container.resolve(DI_TOKENS.LeagueMembershipRepository); const standingRepository = container.resolve(DI_TOKENS.StandingRepository); + const leagueStandingsRepository = container.resolve(DI_TOKENS.LeagueStandingsRepository); const penaltyRepository = container.resolve(DI_TOKENS.PenaltyRepository); const protestRepository = container.resolve(DI_TOKENS.ProtestRepository); const teamRepository = container.resolve(DI_TOKENS.TeamRepository); @@ -1095,10 +1123,14 @@ export function configureDIContainer(): void { new GetRaceRegistrationsUseCase(raceRegistrationRepository) ); - const leagueStandingsPresenter = new LeagueStandingsPresenter(); - container.registerInstance( + container.registerInstance( DI_TOKENS.GetLeagueStandingsUseCase, - new GetLeagueStandingsUseCase(standingRepository), + new GetLeagueStandingsUseCaseImpl(leagueStandingsRepository), + ); + + container.registerInstance( + DI_TOKENS.LeagueStandingsPresenter, + new LeagueStandingsPresenter(container.resolve(DI_TOKENS.GetLeagueStandingsUseCase)), ); container.registerInstance( diff --git a/apps/website/lib/di-tokens.ts b/apps/website/lib/di-tokens.ts index 4f124f179..5b8a06fb5 100644 --- a/apps/website/lib/di-tokens.ts +++ b/apps/website/lib/di-tokens.ts @@ -9,6 +9,7 @@ export const DI_TOKENS = { RaceRepository: Symbol.for('IRaceRepository'), ResultRepository: Symbol.for('IResultRepository'), StandingRepository: Symbol.for('IStandingRepository'), + LeagueStandingsRepository: Symbol.for('ILeagueStandingsRepository'), PenaltyRepository: Symbol.for('IPenaltyRepository'), ProtestRepository: Symbol.for('IProtestRepository'), TeamRepository: Symbol.for('ITeamRepository'), @@ -50,6 +51,18 @@ export const DI_TOKENS = { Logger: Symbol.for('ILogger'), AuthService: Symbol.for('AuthService'), + // Auth dependencies + IdentityProvider: Symbol.for('IdentityProvider'), + SessionPort: Symbol.for('SessionPort'), + + // Use Cases - Auth + StartAuthUseCase: Symbol.for('StartAuthUseCase'), + GetCurrentUserSessionUseCase: Symbol.for('GetCurrentUserSessionUseCase'), + HandleAuthCallbackUseCase: Symbol.for('HandleAuthCallbackUseCase'), + LogoutUseCase: Symbol.for('LogoutUseCase'), + SignupWithEmailUseCase: Symbol.for('SignupWithEmailUseCase'), + LoginWithEmailUseCase: Symbol.for('LoginWithEmailUseCase'), + // Use Cases - Analytics RecordPageViewUseCase: Symbol.for('RecordPageViewUseCase'), RecordEngagementUseCase: Symbol.for('RecordEngagementUseCase'), @@ -141,6 +154,7 @@ export const DI_TOKENS = { DriverStats: Symbol.for('DriverStats'), // Presenters - Racing + LeagueStandingsPresenter: Symbol.for('ILeagueStandingsPresenter'), RaceWithSOFPresenter: Symbol.for('IRaceWithSOFPresenter'), RaceProtestsPresenter: Symbol.for('IRaceProtestsPresenter'), RacePenaltiesPresenter: Symbol.for('IRacePenaltiesPresenter'), diff --git a/apps/website/lib/presenters/LeagueStandingsPresenter.ts b/apps/website/lib/presenters/LeagueStandingsPresenter.ts index a30ffec14..44ddb6b97 100644 --- a/apps/website/lib/presenters/LeagueStandingsPresenter.ts +++ b/apps/website/lib/presenters/LeagueStandingsPresenter.ts @@ -1,41 +1,22 @@ -import type { - ILeagueStandingsPresenter, - LeagueStandingsResultDTO, - LeagueStandingsViewModel, - StandingItemViewModel, -} from '@gridpilot/racing/application/presenters/ILeagueStandingsPresenter'; +import { GetLeagueStandingsUseCase, LeagueStandingsViewModel } from '@gridpilot/core/league/application/use-cases/GetLeagueStandingsUseCase'; + +export interface ILeagueStandingsPresenter { + present(leagueId: string): Promise; + getViewModel(): LeagueStandingsViewModel | null; + reset(): void; +} export class LeagueStandingsPresenter implements ILeagueStandingsPresenter { private viewModel: LeagueStandingsViewModel | null = null; + constructor(private getLeagueStandingsUseCase: GetLeagueStandingsUseCase) {} + reset(): void { this.viewModel = null; } - present(dto: LeagueStandingsResultDTO): void { - const standingItems: StandingItemViewModel[] = dto.standings.map((standing) => { - const raw = standing as unknown as { - seasonId?: string; - podiums?: number; - }; - - return { - id: standing.id, - leagueId: standing.leagueId, - seasonId: raw.seasonId ?? '', - driverId: standing.driverId, - position: standing.position, - points: standing.points, - wins: standing.wins, - podiums: raw.podiums ?? 0, - racesCompleted: standing.racesCompleted, - }; - }); - - this.viewModel = { - leagueId: dto.standings[0]?.leagueId ?? '', - standings: standingItems, - }; + async present(leagueId: string): Promise { + this.viewModel = await this.getLeagueStandingsUseCase.execute(leagueId); } getViewModel(): LeagueStandingsViewModel | null { diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json index c3ff5b843..1a6ad9001 100644 --- a/apps/website/tsconfig.json +++ b/apps/website/tsconfig.json @@ -28,13 +28,9 @@ "@gridpilot/testing-support/*": ["../../core/testing-support/*"], "@gridpilot/media": ["../../core/media/index.ts"], "@gridpilot/media/*": ["../../core/media/*"], - "@gridpilot/shared": ["../../core/shared/index.ts"], - "@gridpilot/shared/application": ["../../core/shared/application"], - "@gridpilot/shared/application/*": ["../../core/shared/application/*"], - "@gridpilot/shared/presentation": ["../../core/shared/presentation"], - "@gridpilot/shared/presentation/*": ["../../core/shared/presentation/*"], - "@gridpilot/shared/domain": ["../../core/shared/domain"], - "@gridpilot/shared/errors": ["../../core/shared/errors"] + "@gridpilot/shared/logging": ["../../core/shared/logging"], + "@gridpilot/shared/*": ["../../core/shared/*"], + "@gridpilot/core/*": ["../../core/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], diff --git a/core/identity/application/use-cases/GetUserUseCase.ts b/core/identity/application/use-cases/GetUserUseCase.ts new file mode 100644 index 000000000..44cc98b79 --- /dev/null +++ b/core/identity/application/use-cases/GetUserUseCase.ts @@ -0,0 +1,14 @@ +import { User } from '../../domain/entities/User'; +import { IUserRepository } from '../../domain/repositories/IUserRepository'; + +export class GetUserUseCase { + constructor(private userRepo: IUserRepository) {} + + async execute(userId: string): Promise { + const stored = await this.userRepo.findById(userId); + if (!stored) { + throw new Error('User not found'); + } + return User.fromStored(stored); + } +} \ No newline at end of file diff --git a/core/identity/application/use-cases/LoginUseCase.ts b/core/identity/application/use-cases/LoginUseCase.ts new file mode 100644 index 000000000..92e3ffb19 --- /dev/null +++ b/core/identity/application/use-cases/LoginUseCase.ts @@ -0,0 +1,29 @@ +import { EmailAddress } from '../../domain/value-objects/EmailAddress'; +import { User } from '../../domain/entities/User'; +import { IAuthRepository } from '../../domain/repositories/IAuthRepository'; +import { IPasswordHashingService } from '../../domain/services/PasswordHashingService'; + +/** + * Application Use Case: LoginUseCase + * + * Handles user login by verifying credentials. + */ +export class LoginUseCase { + constructor( + private authRepo: IAuthRepository, + private passwordService: IPasswordHashingService + ) {} + + async execute(email: string, password: string): Promise { + const emailVO = EmailAddress.create(email); + const user = await this.authRepo.findByEmail(emailVO); + if (!user || !user.getPasswordHash()) { + throw new Error('Invalid credentials'); + } + const isValid = await this.passwordService.verify(password, user.getPasswordHash()!.value); + if (!isValid) { + throw new Error('Invalid credentials'); + } + return user; + } +} \ No newline at end of file diff --git a/core/identity/application/use-cases/SignupUseCase.ts b/core/identity/application/use-cases/SignupUseCase.ts new file mode 100644 index 000000000..6b330efec --- /dev/null +++ b/core/identity/application/use-cases/SignupUseCase.ts @@ -0,0 +1,41 @@ +import { EmailAddress } from '../../domain/value-objects/EmailAddress'; +import { UserId } from '../../domain/value-objects/UserId'; +import { User } from '../../domain/entities/User'; +import { IAuthRepository } from '../../domain/repositories/IAuthRepository'; +import { IPasswordHashingService } from '../../domain/services/PasswordHashingService'; + +/** + * Application Use Case: SignupUseCase + * + * Handles user registration. + */ +export class SignupUseCase { + constructor( + private authRepo: IAuthRepository, + private passwordService: IPasswordHashingService + ) {} + + async execute(email: string, password: string, displayName: string): Promise { + const emailVO = EmailAddress.create(email); + + // Check if user already exists + const existingUser = await this.authRepo.findByEmail(emailVO); + if (existingUser) { + throw new Error('User already exists'); + } + + const hashedPassword = await this.passwordService.hash(password); + const passwordHash = await import('../../domain/value-objects/PasswordHash').then(m => m.PasswordHash.fromHash(hashedPassword)); + + const userId = UserId.create(); + const user = User.create({ + id: userId, + displayName, + email: emailVO.value, + passwordHash, + }); + + await this.authRepo.save(user); + return user; + } +} \ No newline at end of file diff --git a/core/identity/domain/entities/User.ts b/core/identity/domain/entities/User.ts index ce4a29d3e..10fe7a776 100644 --- a/core/identity/domain/entities/User.ts +++ b/core/identity/domain/entities/User.ts @@ -1,11 +1,14 @@ import type { EmailValidationResult } from '../types/EmailAddress'; import { validateEmail } from '../types/EmailAddress'; import { UserId } from '../value-objects/UserId'; +import { PasswordHash } from '../value-objects/PasswordHash'; +import { StoredUser } from '../repositories/IUserRepository'; export interface UserProps { id: UserId; displayName: string; email?: string; + passwordHash?: PasswordHash; iracingCustomerId?: string; primaryDriverId?: string; avatarUrl?: string; @@ -15,6 +18,7 @@ export class User { private readonly id: UserId; private displayName: string; private email: string | undefined; + private passwordHash: PasswordHash | undefined; private iracingCustomerId: string | undefined; private primaryDriverId: string | undefined; private avatarUrl: string | undefined; @@ -47,6 +51,22 @@ export class User { return new User(props); } + public static fromStored(stored: StoredUser): User { + const passwordHash = stored.passwordHash ? PasswordHash.fromHash(stored.passwordHash) : undefined; + const userProps: any = { + id: UserId.fromString(stored.id), + displayName: stored.displayName, + email: stored.email, + }; + if (passwordHash) { + userProps.passwordHash = passwordHash; + } + if (stored.primaryDriverId) { + userProps.primaryDriverId = stored.primaryDriverId; + } + return new User(userProps); + } + public getId(): UserId { return this.id; } @@ -59,6 +79,10 @@ export class User { return this.email; } + public getPasswordHash(): PasswordHash | undefined { + return this.passwordHash; + } + public getIracingCustomerId(): string | undefined { return this.iracingCustomerId; } diff --git a/core/identity/domain/repositories/IAuthRepository.ts b/core/identity/domain/repositories/IAuthRepository.ts new file mode 100644 index 000000000..1cd5140b9 --- /dev/null +++ b/core/identity/domain/repositories/IAuthRepository.ts @@ -0,0 +1,19 @@ +import { EmailAddress } from '../value-objects/EmailAddress'; +import { User } from '../entities/User'; + +/** + * Domain Repository: IAuthRepository + * + * Repository interface for authentication operations. + */ +export interface IAuthRepository { + /** + * Find user by email + */ + findByEmail(email: EmailAddress): Promise; + + /** + * Save a user + */ + save(user: User): Promise; +} \ No newline at end of file diff --git a/core/identity/domain/services/PasswordHashingService.ts b/core/identity/domain/services/PasswordHashingService.ts new file mode 100644 index 000000000..93458aac3 --- /dev/null +++ b/core/identity/domain/services/PasswordHashingService.ts @@ -0,0 +1,26 @@ +import { PasswordHash } from '../value-objects/PasswordHash'; + +/** + * Domain Service: PasswordHashingService + * + * Service for password hashing and verification. + */ +export interface IPasswordHashingService { + hash(plain: string): Promise; + verify(plain: string, hash: string): Promise; +} + +/** + * Implementation using bcrypt via PasswordHash VO. + */ +export class PasswordHashingService implements IPasswordHashingService { + async hash(plain: string): Promise { + const passwordHash = await PasswordHash.create(plain); + return passwordHash.value; + } + + async verify(plain: string, hash: string): Promise { + const passwordHash = PasswordHash.fromHash(hash); + return passwordHash.verify(plain); + } +} \ No newline at end of file diff --git a/core/identity/domain/value-objects/PasswordHash.ts b/core/identity/domain/value-objects/PasswordHash.ts new file mode 100644 index 000000000..8aacd944e --- /dev/null +++ b/core/identity/domain/value-objects/PasswordHash.ts @@ -0,0 +1,41 @@ +import bcrypt from 'bcrypt'; +import type { IValueObject } from '@gridpilot/shared/domain'; + +export interface PasswordHashProps { + value: string; +} + +/** + * Value Object: PasswordHash + * + * Wraps a bcrypt-hashed password string and provides verification. + */ +export class PasswordHash implements IValueObject { + public readonly props: PasswordHashProps; + + private constructor(value: string) { + this.props = { value }; + } + + static async create(plain: string): Promise { + const saltRounds = 12; + const hash = await bcrypt.hash(plain, saltRounds); + return new PasswordHash(hash); + } + + static fromHash(hash: string): PasswordHash { + return new PasswordHash(hash); + } + + get value(): string { + return this.props.value; + } + + async verify(plain: string): Promise { + return bcrypt.compare(plain, this.props.value); + } + + equals(other: IValueObject): boolean { + return this.props.value === other.props.value; + } +} \ No newline at end of file diff --git a/core/identity/domain/value-objects/UserId.ts b/core/identity/domain/value-objects/UserId.ts index 1ebe9c022..06ca37d37 100644 --- a/core/identity/domain/value-objects/UserId.ts +++ b/core/identity/domain/value-objects/UserId.ts @@ -1,3 +1,4 @@ +import { v4 as uuidv4 } from 'uuid'; import type { IValueObject } from '@gridpilot/shared/domain'; export interface UserIdProps { @@ -14,6 +15,10 @@ export class UserId implements IValueObject { this.props = { value }; } + public static create(): UserId { + return new UserId(uuidv4()); + } + public static fromString(value: string): UserId { return new UserId(value); } diff --git a/core/league/application/ports/ILeagueStandingsRepository.ts b/core/league/application/ports/ILeagueStandingsRepository.ts new file mode 100644 index 000000000..ce1fbe4a2 --- /dev/null +++ b/core/league/application/ports/ILeagueStandingsRepository.ts @@ -0,0 +1,16 @@ +export interface RawStanding { + id: string; + leagueId: string; + driverId: string; + position: number; + points: number; + wins: number; + racesCompleted: number; + // These properties might be optional or present depending on the data source + seasonId?: string; + podiums?: number; +} + +export interface ILeagueStandingsRepository { + getLeagueStandings(leagueId: string): Promise; +} diff --git a/core/league/application/use-cases/GetLeagueStandingsUseCase.ts b/core/league/application/use-cases/GetLeagueStandingsUseCase.ts new file mode 100644 index 000000000..c5193b81d --- /dev/null +++ b/core/league/application/use-cases/GetLeagueStandingsUseCase.ts @@ -0,0 +1,20 @@ +export interface GetLeagueStandingsUseCase { + execute(leagueId: string): Promise; +} + +export interface StandingItemViewModel { + id: string; + leagueId: string; + seasonId: string; + driverId: string; + position: number; + points: number; + wins: number; + podiums: number; + racesCompleted: number; +} + +export interface LeagueStandingsViewModel { + leagueId: string; + standings: StandingItemViewModel[]; +} diff --git a/core/league/application/use-cases/GetLeagueStandingsUseCaseImpl.ts b/core/league/application/use-cases/GetLeagueStandingsUseCaseImpl.ts new file mode 100644 index 000000000..5ec48a64d --- /dev/null +++ b/core/league/application/use-cases/GetLeagueStandingsUseCaseImpl.ts @@ -0,0 +1,29 @@ +import { GetLeagueStandingsUseCase, LeagueStandingsViewModel, StandingItemViewModel } from './GetLeagueStandingsUseCase'; +import { ILeagueStandingsRepository, RawStanding } from '../ports/ILeagueStandingsRepository'; + +export class GetLeagueStandingsUseCaseImpl implements GetLeagueStandingsUseCase { + constructor(private repository: ILeagueStandingsRepository) {} + + async execute(leagueId: string): Promise { + const rawStandings = await this.repository.getLeagueStandings(leagueId); + + const standingItems: StandingItemViewModel[] = rawStandings.map((standing: RawStanding) => { + return { + id: standing.id, + leagueId: standing.leagueId, + seasonId: standing.seasonId ?? '', + driverId: standing.driverId, + position: standing.position, + points: standing.points, + wins: standing.wins, + podiums: standing.podiums ?? 0, + racesCompleted: standing.racesCompleted, + }; + }); + + return { + leagueId: leagueId, + standings: standingItems, + }; + } +} diff --git a/core/testing-support/index.ts b/core/testing-support/index.ts deleted file mode 100644 index aab79a2f9..000000000 --- a/core/testing-support/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from './src/faker/faker'; -export * from './src/images/images'; -export * from './src/media/DemoAvatarGenerationAdapter'; -export * from './src/media/DemoFaceValidationAdapter'; -export * from './src/media/DemoImageServiceAdapter'; -export * from './src/media/InMemoryAvatarGenerationRepository'; -export * from './src/racing/RacingSeedCore'; -export * from './src/racing/RacingSponsorshipSeed'; -export * from './src/racing/RacingFeedSeed'; -export * from './src/racing/RacingStaticSeed'; -export * from './src/racing/DemoTracks'; -export * from './src/racing/DemoCars'; -export * from './src/racing/DemoDriverStats'; \ No newline at end of file diff --git a/core/testing-support/package.json b/core/testing-support/package.json deleted file mode 100644 index 4342e0f71..000000000 --- a/core/testing-support/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@gridpilot/testing", - "version": "0.1.0", - "private": true, - "main": "./index.ts", - "types": "./index.ts" -} \ No newline at end of file diff --git a/core/testing-support/tsconfig.json b/core/testing-support/tsconfig.json deleted file mode 100644 index 91badd622..000000000 --- a/core/testing-support/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "declaration": true, - "declarationMap": false - }, - "include": ["**/*.ts"] -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 348ccf33e..d45beec63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,16 +14,19 @@ ], "dependencies": { "@gridpilot/social": "file:core/social", + "bcrypt": "^6.0.0", "playwright-extra": "^4.3.6", "puppeteer-extra-plugin-stealth": "^2.11.2", "reflect-metadata": "^0.2.2", - "tsyringe": "^4.10.0" + "tsyringe": "^4.10.0", + "uuid": "^13.0.0" }, "devDependencies": { "@cucumber/cucumber": "^11.0.1", "@playwright/test": "^1.57.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", + "@types/bcrypt": "^6.0.0", "@types/express": "^4.17.21", "@types/jsdom": "^27.0.0", "@types/node": "^24.10.1", @@ -323,6 +326,92 @@ "node": ">=10.13.0" } }, + "apps/website/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "core/analytics": { + "name": "@gridpilot/analytics", + "version": "0.1.0" + }, + "core/automation": { + "name": "@gridpilot/automation", + "version": "0.1.0" + }, + "core/identity": { + "name": "@gridpilot/identity", + "version": "0.1.0", + "dependencies": { + "zod": "^3.25.76" + } + }, + "core/media": { + "name": "@gridpilot/media", + "version": "0.1.0" + }, + "core/notifications": { + "name": "@gridpilot/notifications", + "version": "0.1.0", + "dependencies": { + "uuid": "^11.0.5" + }, + "devDependencies": { + "@types/uuid": "^10.0.0" + } + }, + "core/notifications/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "core/racing": { + "name": "@gridpilot/racing", + "version": "0.1.0", + "dependencies": { + "uuid": "^11.0.5" + }, + "devDependencies": { + "@types/uuid": "^10.0.0" + } + }, + "core/racing/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "core/social": { + "name": "@gridpilot/social", + "version": "0.1.0" + }, + "core/testing-support": { + "name": "@gridpilot/testing-support", + "version": "0.1.0" + }, "node_modules/@adobe/css-tools": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", @@ -4088,6 +4177,16 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -5925,6 +6024,20 @@ "node": ">=10.0.0" } }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -12115,6 +12228,15 @@ "tslib": "^2.0.3" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -12157,6 +12279,17 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -16750,6 +16883,19 @@ "ieee754": "^1.2.1" } }, + "node_modules/typeorm/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -16992,16 +17138,16 @@ } }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/esm/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { @@ -17649,53 +17795,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "core/analytics": { - "name": "@gridpilot/analytics", - "version": "0.1.0" - }, - "core/automation": { - "name": "@gridpilot/automation", - "version": "0.1.0" - }, - "core/identity": { - "name": "@gridpilot/identity", - "version": "0.1.0", - "dependencies": { - "zod": "^3.25.76" - } - }, - "core/media": { - "name": "@gridpilot/media", - "version": "0.1.0" - }, - "core/notifications": { - "name": "@gridpilot/notifications", - "version": "0.1.0", - "dependencies": { - "uuid": "^11.0.5" - }, - "devDependencies": { - "@types/uuid": "^10.0.0" - } - }, - "core/racing": { - "name": "@gridpilot/racing", - "version": "0.1.0", - "dependencies": { - "uuid": "^11.0.5" - }, - "devDependencies": { - "@types/uuid": "^10.0.0" - } - }, - "core/social": { - "name": "@gridpilot/social", - "version": "0.1.0" - }, - "core/testing-support": { - "name": "@gridpilot/testing-support", - "version": "0.1.0" } } } diff --git a/package.json b/package.json index 01679bd4d..1eba77866 100644 --- a/package.json +++ b/package.json @@ -67,9 +67,10 @@ "@playwright/test": "^1.57.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", + "@types/bcrypt": "^6.0.0", + "@types/express": "^4.17.21", "@types/jsdom": "^27.0.0", "@types/node": "^24.10.1", - "@types/express": "^4.17.21", "@vitest/ui": "^2.1.8", "cheerio": "^1.0.0", "commander": "^11.0.0", @@ -85,9 +86,11 @@ }, "dependencies": { "@gridpilot/social": "file:core/social", + "bcrypt": "^6.0.0", "playwright-extra": "^4.3.6", "puppeteer-extra-plugin-stealth": "^2.11.2", "reflect-metadata": "^0.2.2", - "tsyringe": "^4.10.0" + "tsyringe": "^4.10.0", + "uuid": "^13.0.0" } } diff --git a/plans/auth-clean-arch.md b/plans/auth-clean-arch.md new file mode 100644 index 000000000..c04d9453c --- /dev/null +++ b/plans/auth-clean-arch.md @@ -0,0 +1,180 @@ +# Clean Architecture Compliant Auth Layer Design + +## Current State Analysis (Diagnosis) +- Existing [`core/identity/index.ts](core/identity/index.ts) exports User entity, IUserRepository port, but lacks credential-based auth ports (IAuthRepository), value objects (PasswordHash), and use cases (LoginUseCase, SignupUseCase). +- iRacing-specific auth use cases present (StartAuthUseCase, HandleAuthCallbackUseCase), but no traditional email/password flows. +- No domain services for password hashing/validation. +- In-memory impls limited to ratings/achievements; missing User/Auth repo impls. +- [`apps/website/lib/auth/InMemoryAuthService.ts](apps/website/lib/auth/InMemoryAuthService.ts) (visible) embeds business logic, violating dependency inversion. +- App-layer AuthService exists but thick; no thin delivery via DI-injected use cases. +- No AuthContext integration with clean AuthService. +- DI setup in [`apps/website/lib/di-tokens.ts](apps/website/lib/di-tokens.ts) etc. needs auth bindings. + +## Architectural Plan +1. Add `PasswordHash` value object in `core/identity/domain/value-objects/PasswordHash.ts`. +2. Create `IAuthRepository` port in `core/identity/domain/repositories/IAuthRepository.ts` with `findByCredentials(email: EmailAddress, passwordHash: PasswordHash): Promise`, `save(user: User): Promise`. +3. Extend `IUserRepository` if needed for non-auth user ops. +4. Implement `InMemoryUserRepository` in `adapters/identity/inmem/InMemoryUserRepository.ts` satisfying `IUserRepository & IAuthRepository`. +5. Add domain service `PasswordHashingService` in `core/identity/domain/services/PasswordHashingService.ts` (interface + dummy impl). +6. Create `LoginUseCase` in `core/identity/application/use-cases/LoginUseCase.ts` orchestrating repo find + hashing service. +7. Create `SignupUseCase` in `core/identity/application/use-cases/SignupUseCase.ts` validating, hashing, save via repos. +8. Create `GetUserUseCase` in `core/identity/application/use-cases/GetUserUseCase.ts` via IUserRepository. +9. Refactor `apps/website/lib/auth/AuthService.ts` as thin adapter: DTO -> use case calls via injected deps. +10. Update `apps/website/lib/auth/AuthContext.tsx` to provide/use AuthService via React Context. +11. Add DI tokens in `apps/website/lib/di-tokens.ts`: `AUTH_REPOSITORY_TOKEN`, `USER_REPOSITORY_TOKEN`, `LOGIN_USE_CASE_TOKEN`, etc. +12. Bind in `apps/website/lib/di-config.ts` / `di-container.ts`: ports -> inmem impls, use cases -> deps, AuthService -> use cases. + +## Summary +Layer auth into domain entities/VOs/services, application use cases/ports, infrastructure adapters (inmem), thin app delivery (AuthService) wired via DI. Coexists with existing iRacing provider auth. + +## Design Overview +Follows strict Clean Architecture: +- **Entities**: User with EmailAddress, PasswordHash VOs. +- **Use Cases**: Pure orchestrators, depend on ports/services. +- **Ports**: IAuthRepository (credential ops), IUserRepository (user data). +- **Adapters**: Inmem impls. +- **Delivery**: AuthService maps HTTP/JS DTOs to use cases. +- **DI**: Inversion via tokens/container. + +```mermaid +graph TB + DTO[DTOs
apps/website/lib/auth] --> AuthService[AuthService
apps/website/lib/auth] + AuthService --> LoginUC[LoginUseCase
core/identity/application] + AuthService --> SignupUC[SignupUseCase] + LoginUC --> IAuthRepo[IAuthRepository
core/identity/domain] + SignupUC --> PasswordSvc[PasswordHashingService
core/identity/domain] + IAuthRepo --> InMemRepo[InMemoryUserRepository
adapters/identity/inmem] + AuthService -.-> DI[DI Container
apps/website/lib/di-*] + AuthContext[AuthContext.tsx] --> AuthService +``` + +## Files Structure +``` +core/identity/ +├── domain/ +│ ├── value-objects/PasswordHash.ts (new) +│ ├── entities/User.ts (extend if needed) +│ ├── repositories/IAuthRepository.ts (new) +│ └── services/PasswordHashingService.ts (new) +├── application/ +│ └── use-cases/LoginUseCase.ts (new) +│ SignupUseCase.ts (new) +│ GetUserUseCase.ts (new) +adapters/identity/inmem/ +├── InMemoryUserRepository.ts (new) +apps/website/lib/auth/ +├── AuthService.ts (refactor) +└── AuthContext.tsx (update) +apps/website/lib/ +├── di-tokens.ts (update) +├── di-config.ts (update) +└── di-container.ts (update) +``` + +## Code Snippets + +### PasswordHash VO +```ts +// core/identity/domain/value-objects/PasswordHash.ts +import { ValueObject } from '../../../shared/domain/ValueObject'; // assume shared + +export class PasswordHash extends ValueObject { + static create(plain: string): PasswordHash { + // dummy bcrypt hash + return new PasswordHash(btoa(plain)); // prod: use bcrypt + } + + verify(plain: string): boolean { + return btoa(plain) === this.value; + } +} +``` + +### IAuthRepository Port +```ts +// core/identity/domain/repositories/IAuthRepository.ts +import { UserId, EmailAddress } from '../value-objects'; +import { User } from '../entities/User'; +import { PasswordHash } from '../value-objects/PasswordHash'; + +export interface IAuthRepository { + findByCredentials(email: EmailAddress, passwordHash: PasswordHash): Promise; + save(user: User): Promise; +} +``` + +### LoginUseCase +```ts +// core/identity/application/use-cases/LoginUseCase.ts +import { Injectable } from 'di'; // assume +import { IAuthRepository } from '../../domain/repositories/IAuthRepository'; +import { PasswordHash } from '../../domain/value-objects/PasswordHash'; +import { EmailAddress } from '../../domain/value-objects/EmailAddress'; +import { User } from '../../domain/entities/User'; + +export class LoginUseCase { + constructor( + private authRepo: IAuthRepository + ) {} + + async execute(email: string, password: string): Promise { + const emailVO = EmailAddress.create(email); + const passwordHash = PasswordHash.create(password); + const user = await this.authRepo.findByCredentials(emailVO, passwordHash); + if (!user) throw new Error('Invalid credentials'); + return user; + } +} +``` + +### AuthService (Thin Adapter) +```ts +// apps/website/lib/auth/AuthService.ts +import { injectable, inject } from 'di'; // assume +import { LoginUseCase } from '@gridpilot/identity'; +import type { LoginDto } from '@gridpilot/identity/application/dto'; // define DTO + +@injectable() +export class AuthService { + constructor( + @inject(LoginUseCase_TOKEN) private loginUC: LoginUseCase + ) {} + + async login(credentials: {email: string, password: string}) { + return await this.loginUC.execute(credentials.email, credentials.password); + } + // similar for signup, getUser +} +``` + +### DI Tokens Update +```ts +// apps/website/lib/di-tokens.ts +export const LOGIN_USE_CASE_TOKEN = Symbol('LoginUseCase'); +export const AUTH_REPOSITORY_TOKEN = Symbol('IAuthRepository'); +// etc. +``` + +### AuthContext Usage Example +```tsx +// apps/website/lib/auth/AuthContext.tsx +import { createContext, useContext } from 'react'; +import { AuthService } from './AuthService'; +import { diContainer } from '../di-container'; + +const AuthContext = createContext(null!); + +export function AuthProvider({ children }) { + const authService = diContainer.resolve(AuthService); + return {children}; +} + +export const useAuth = () => useContext(AuthContext); +``` + +## Implementation Notes +- Update `core/identity/index.ts` exports for new modules. +- Update `core/identity/package.json` exports if needed. +- Use dummy hashing for dev; prod adapter swaps repo impl. +- No business logic in app/website: all in core use cases. +- Coexists with iRacing auth: separate use cases/services. diff --git a/testing/factories/racing/ChampionshipConfigFactory.ts b/testing/factories/racing/ChampionshipConfigFactory.ts new file mode 100644 index 000000000..a4eed2d84 --- /dev/null +++ b/testing/factories/racing/ChampionshipConfigFactory.ts @@ -0,0 +1,49 @@ +import type { SessionType } from '@gridpilot/racing/domain/types/SessionType'; +import { PointsTable } from '@gridpilot/racing/domain/value-objects/PointsTable'; +import type { BonusRule } from '@gridpilot/racing/domain/types/BonusRule'; +import type { ChampionshipConfig } from '@gridpilot/racing/domain/types/ChampionshipConfig'; +import { makePointsTable } from './PointsTableFactory'; + +export const makeChampionshipConfig = (params: { + id: string; + name: string; + sessionTypes: SessionType[]; + mainPoints: number[]; + sprintPoints?: number[]; + mainBonusRules?: BonusRule[]; +}): ChampionshipConfig => { + const { id, name, sessionTypes, mainPoints, sprintPoints, mainBonusRules } = params; + + const pointsTableBySessionType: Record = {} as Record; + + sessionTypes.forEach((sessionType) => { + if (sessionType === 'main') { + pointsTableBySessionType[sessionType] = makePointsTable(mainPoints); + } else if (sessionType === 'sprint' && sprintPoints) { + pointsTableBySessionType[sessionType] = makePointsTable(sprintPoints); + } else { + pointsTableBySessionType[sessionType] = new PointsTable({}); + } + }); + + const bonusRulesBySessionType: Record = {} as Record; + sessionTypes.forEach((sessionType) => { + if (sessionType === 'main' && mainBonusRules) { + bonusRulesBySessionType[sessionType] = mainBonusRules; + } else { + bonusRulesBySessionType[sessionType] = []; + } + }); + + return { + id, + name, + type: 'driver', + sessionTypes, + pointsTableBySessionType, + bonusRulesBySessionType, + dropScorePolicy: { + strategy: 'none', + }, + }; +}; \ No newline at end of file diff --git a/testing/factories/racing/DriverRefFactory.ts b/testing/factories/racing/DriverRefFactory.ts new file mode 100644 index 000000000..bf31cb5a5 --- /dev/null +++ b/testing/factories/racing/DriverRefFactory.ts @@ -0,0 +1,7 @@ +import type { ParticipantRef } from '@gridpilot/racing/domain/types/ParticipantRef'; +import type { ChampionshipType } from '@gridpilot/racing/domain/types/ChampionshipType'; + +export const makeDriverRef = (id: string): ParticipantRef => ({ + type: 'driver' as ChampionshipType, + id, +}); \ No newline at end of file diff --git a/testing/factories/racing/PointsTableFactory.ts b/testing/factories/racing/PointsTableFactory.ts new file mode 100644 index 000000000..ff63cdc29 --- /dev/null +++ b/testing/factories/racing/PointsTableFactory.ts @@ -0,0 +1,9 @@ +import { PointsTable } from '@gridpilot/racing/domain/value-objects/PointsTable'; + +export const makePointsTable = (points: number[]): PointsTable => { + const pointsByPosition: Record = {}; + points.forEach((value, index) => { + pointsByPosition[index + 1] = value; + }); + return new PointsTable(pointsByPosition); +}; \ No newline at end of file diff --git a/testing/factories/racing/SeasonFactory.ts b/testing/factories/racing/SeasonFactory.ts new file mode 100644 index 000000000..ab95b0cde --- /dev/null +++ b/testing/factories/racing/SeasonFactory.ts @@ -0,0 +1,23 @@ +import { Season } from '@gridpilot/racing/domain/entities/Season'; +import type { SeasonStatus } from '@gridpilot/racing/domain/entities/Season'; + +export const createMinimalSeason = (overrides?: { status?: SeasonStatus }) => + Season.create({ + id: 'season-1', + leagueId: 'league-1', + gameId: 'iracing', + name: 'Test Season', + status: overrides?.status ?? 'planned', + }); + +export const createBaseSeason = () => + Season.create({ + id: 'season-1', + leagueId: 'league-1', + gameId: 'iracing', + name: 'Config Season', + status: 'planned', + startDate: new Date('2025-01-01T00:00:00Z'), + endDate: undefined, + maxDrivers: 24, + }); \ No newline at end of file diff --git a/core/identity/infrastructure/providers/IracingDemoIdentityProviderAdapter.ts b/testing/fakes/identity/IracingDemoIdentityProviderAdapter.ts similarity index 100% rename from core/identity/infrastructure/providers/IracingDemoIdentityProviderAdapter.ts rename to testing/fakes/identity/IracingDemoIdentityProviderAdapter.ts diff --git a/core/testing-support/src/media/DemoAvatarGenerationAdapter.ts b/testing/fakes/media/DemoAvatarGenerationAdapter.ts similarity index 100% rename from core/testing-support/src/media/DemoAvatarGenerationAdapter.ts rename to testing/fakes/media/DemoAvatarGenerationAdapter.ts diff --git a/core/testing-support/src/media/DemoFaceValidationAdapter.ts b/testing/fakes/media/DemoFaceValidationAdapter.ts similarity index 100% rename from core/testing-support/src/media/DemoFaceValidationAdapter.ts rename to testing/fakes/media/DemoFaceValidationAdapter.ts diff --git a/core/testing-support/src/media/DemoImageServiceAdapter.ts b/testing/fakes/media/DemoImageServiceAdapter.ts similarity index 100% rename from core/testing-support/src/media/DemoImageServiceAdapter.ts rename to testing/fakes/media/DemoImageServiceAdapter.ts diff --git a/core/testing-support/src/racing/DemoCars.ts b/testing/fakes/racing/DemoCars.ts similarity index 100% rename from core/testing-support/src/racing/DemoCars.ts rename to testing/fakes/racing/DemoCars.ts diff --git a/core/testing-support/src/racing/DemoDriverStats.ts b/testing/fakes/racing/DemoDriverStats.ts similarity index 100% rename from core/testing-support/src/racing/DemoDriverStats.ts rename to testing/fakes/racing/DemoDriverStats.ts diff --git a/core/testing-support/src/racing/DemoTracks.ts b/testing/fakes/racing/DemoTracks.ts similarity index 100% rename from core/testing-support/src/racing/DemoTracks.ts rename to testing/fakes/racing/DemoTracks.ts diff --git a/core/testing-support/src/racing/RacingFeedSeed.ts b/testing/fixtures/racing/RacingFeedSeed.ts similarity index 98% rename from core/testing-support/src/racing/RacingFeedSeed.ts rename to testing/fixtures/racing/RacingFeedSeed.ts index 84f871f8b..a0cb2ff06 100644 --- a/core/testing-support/src/racing/RacingFeedSeed.ts +++ b/testing/fixtures/racing/RacingFeedSeed.ts @@ -4,8 +4,8 @@ import { Race } from '@gridpilot/racing/domain/entities/Race'; import type { Result } from '@gridpilot/racing/domain/entities/Result'; import type { FeedItem } from '@gridpilot/social/domain/types/FeedItem'; import type { FriendDTO } from '@gridpilot/social/application/dto/FriendDTO'; -import { faker } from '../faker/faker'; -import { getLeagueBanner, getDriverAvatar } from '../images/images'; +import { faker } from '../../helpers/faker/faker'; +import { getLeagueBanner, getDriverAvatar } from '../../helpers/images/images'; import type { Friendship, RacingMembership } from './RacingSeedCore'; /** diff --git a/core/testing-support/src/racing/RacingSeedCore.ts b/testing/fixtures/racing/RacingSeedCore.ts similarity index 98% rename from core/testing-support/src/racing/RacingSeedCore.ts rename to testing/fixtures/racing/RacingSeedCore.ts index 1c0aff4e4..f271fe63c 100644 --- a/core/testing-support/src/racing/RacingSeedCore.ts +++ b/testing/fixtures/racing/RacingSeedCore.ts @@ -3,7 +3,8 @@ import { League } from '@gridpilot/racing/domain/entities/League'; import { Race } from '@gridpilot/racing/domain/entities/Race'; import { Result } from '@gridpilot/racing/domain/entities/Result'; import { Standing } from '@gridpilot/racing/domain/entities/Standing'; -import { faker } from '../faker/faker'; +import { SessionType } from '@gridpilot/racing/domain/value-objects/SessionType'; +import { faker } from '../../helpers/faker/faker'; /** * Core racing seed types and generators (drivers, leagues, teams, races, standings). @@ -292,7 +293,7 @@ export function createRaces(leagues: League[]): Race[] { scheduledAt, track: faker.helpers.arrayElement(tracks), car: faker.helpers.arrayElement(cars), - sessionType: 'race', + sessionType: SessionType.main(), status, ...(strengthOfField !== undefined ? { strengthOfField } : {}), ...(status === 'running' ? { registeredCount: faker.number.int({ min: 12, max: 20 }) } : {}), diff --git a/core/testing-support/src/racing/RacingSponsorshipSeed.ts b/testing/fixtures/racing/RacingSponsorshipSeed.ts similarity index 100% rename from core/testing-support/src/racing/RacingSponsorshipSeed.ts rename to testing/fixtures/racing/RacingSponsorshipSeed.ts diff --git a/core/testing-support/src/racing/RacingStaticSeed.ts b/testing/fixtures/racing/RacingStaticSeed.ts similarity index 98% rename from core/testing-support/src/racing/RacingStaticSeed.ts rename to testing/fixtures/racing/RacingStaticSeed.ts index dc00f28a8..dc8c102f6 100644 --- a/core/testing-support/src/racing/RacingStaticSeed.ts +++ b/testing/fixtures/racing/RacingStaticSeed.ts @@ -7,8 +7,8 @@ import { Standing } from '@gridpilot/racing/domain/entities/Standing'; import type { FeedItem } from '@gridpilot/social/domain/types/FeedItem'; import type { FriendDTO } from '@gridpilot/social/application/dto/FriendDTO'; -import { faker } from '../faker/faker'; -import { getTeamLogo } from '../images/images'; +import { faker } from '../../helpers/faker/faker'; +import { getTeamLogo } from '../../helpers/images/images'; import { createDrivers, diff --git a/core/testing-support/src/faker/faker.ts b/testing/helpers/faker/faker.ts similarity index 100% rename from core/testing-support/src/faker/faker.ts rename to testing/helpers/faker/faker.ts diff --git a/core/testing-support/src/images/images.ts b/testing/helpers/images/images.ts similarity index 100% rename from core/testing-support/src/images/images.ts rename to testing/helpers/images/images.ts diff --git a/tests/unit/analytics/domain/value-objects/AnalyticsEntityId.test.ts b/tests/analytics/AnalyticsEntityId.spec.ts similarity index 89% rename from tests/unit/analytics/domain/value-objects/AnalyticsEntityId.test.ts rename to tests/analytics/AnalyticsEntityId.spec.ts index 15b06b4de..5ac37362a 100644 --- a/tests/unit/analytics/domain/value-objects/AnalyticsEntityId.test.ts +++ b/tests/analytics/AnalyticsEntityId.spec.ts @@ -1,4 +1,4 @@ -import { AnalyticsEntityId } from '../../../../../core/analytics/domain/value-objects/AnalyticsEntityId'; +import { AnalyticsEntityId } from '../../../core/analytics/domain/value-objects/AnalyticsEntityId'; describe('AnalyticsEntityId', () => { it('creates a valid AnalyticsEntityId from a non-empty string', () => { diff --git a/tests/unit/analytics/domain/value-objects/AnalyticsSessionId.test.ts b/tests/analytics/AnalyticsSessionId.spec.ts similarity index 89% rename from tests/unit/analytics/domain/value-objects/AnalyticsSessionId.test.ts rename to tests/analytics/AnalyticsSessionId.spec.ts index 11e3ed1b6..bbfde4f66 100644 --- a/tests/unit/analytics/domain/value-objects/AnalyticsSessionId.test.ts +++ b/tests/analytics/AnalyticsSessionId.spec.ts @@ -1,4 +1,4 @@ -import { AnalyticsSessionId } from '../../../../../core/analytics/domain/value-objects/AnalyticsSessionId'; +import { AnalyticsSessionId } from '../../../core/analytics/domain/value-objects/AnalyticsSessionId'; describe('AnalyticsSessionId', () => { it('creates a valid AnalyticsSessionId from a non-empty string', () => { diff --git a/tests/unit/analytics/domain/value-objects/PageViewId.test.ts b/tests/analytics/PageViewId.spec.ts similarity index 89% rename from tests/unit/analytics/domain/value-objects/PageViewId.test.ts rename to tests/analytics/PageViewId.spec.ts index 75adebb2e..947adae61 100644 --- a/tests/unit/analytics/domain/value-objects/PageViewId.test.ts +++ b/tests/analytics/PageViewId.spec.ts @@ -1,4 +1,4 @@ -import { PageViewId } from '../../../../../core/analytics/domain/value-objects/PageViewId'; +import { PageViewId } from '../../../core/analytics/domain/value-objects/PageViewId'; describe('PageViewId', () => { it('creates a valid PageViewId from a non-empty string', () => { diff --git a/tests/unit/application/use-cases/CheckAuthenticationUseCase.test.ts b/tests/application/CheckAuthenticationUseCase.spec.ts similarity index 100% rename from tests/unit/application/use-cases/CheckAuthenticationUseCase.test.ts rename to tests/application/CheckAuthenticationUseCase.spec.ts diff --git a/tests/unit/application/use-cases/CompleteRaceCreationUseCase.test.ts b/tests/application/CompleteRaceCreationUseCase.spec.ts similarity index 100% rename from tests/unit/application/use-cases/CompleteRaceCreationUseCase.test.ts rename to tests/application/CompleteRaceCreationUseCase.spec.ts diff --git a/tests/unit/application/use-cases/ConfirmCheckoutUseCase.enhanced.test.ts b/tests/application/ConfirmCheckoutUseCase.enhanced.spec.ts similarity index 100% rename from tests/unit/application/use-cases/ConfirmCheckoutUseCase.enhanced.test.ts rename to tests/application/ConfirmCheckoutUseCase.enhanced.spec.ts diff --git a/tests/unit/application/use-cases/ConfirmCheckoutUseCase.test.ts b/tests/application/ConfirmCheckoutUseCase.spec.ts similarity index 100% rename from tests/unit/application/use-cases/ConfirmCheckoutUseCase.test.ts rename to tests/application/ConfirmCheckoutUseCase.spec.ts diff --git a/tests/unit/application/ports/ICheckoutConfirmationPort.test.ts b/tests/application/ICheckoutConfirmationPort.spec.ts similarity index 100% rename from tests/unit/application/ports/ICheckoutConfirmationPort.test.ts rename to tests/application/ICheckoutConfirmationPort.spec.ts diff --git a/tests/unit/application/services/OverlaySyncService.test.ts b/tests/application/OverlaySyncService.spec.ts similarity index 100% rename from tests/unit/application/services/OverlaySyncService.test.ts rename to tests/application/OverlaySyncService.spec.ts diff --git a/tests/unit/application/services/OverlaySyncService.timeout.test.ts b/tests/application/OverlaySyncService.timeout.spec.ts similarity index 100% rename from tests/unit/application/services/OverlaySyncService.timeout.test.ts rename to tests/application/OverlaySyncService.timeout.spec.ts diff --git a/tests/unit/application/use-cases/RecalculateChampionshipStandingsUseCase.test.ts b/tests/application/RecalculateChampionshipStandingsUseCase.spec.ts similarity index 100% rename from tests/unit/application/use-cases/RecalculateChampionshipStandingsUseCase.test.ts rename to tests/application/RecalculateChampionshipStandingsUseCase.spec.ts diff --git a/tests/unit/application/use-cases/StartAutomationSession.test.ts b/tests/application/StartAutomationSession.spec.ts similarity index 100% rename from tests/unit/application/use-cases/StartAutomationSession.test.ts rename to tests/application/StartAutomationSession.spec.ts diff --git a/tests/unit/application/use-cases/VerifyAuthenticatedPageUseCase.test.ts b/tests/application/VerifyAuthenticatedPageUseCase.spec.ts similarity index 100% rename from tests/unit/application/use-cases/VerifyAuthenticatedPageUseCase.test.ts rename to tests/application/VerifyAuthenticatedPageUseCase.spec.ts diff --git a/tests/unit/domain/entities/AutomationSession.test.ts b/tests/domain/AutomationSession.spec.ts similarity index 100% rename from tests/unit/domain/entities/AutomationSession.test.ts rename to tests/domain/AutomationSession.spec.ts diff --git a/tests/unit/domain/value-objects/BrowserAuthenticationState.test.ts b/tests/domain/BrowserAuthenticationState.spec.ts similarity index 100% rename from tests/unit/domain/value-objects/BrowserAuthenticationState.test.ts rename to tests/domain/BrowserAuthenticationState.spec.ts diff --git a/tests/unit/domain/value-objects/CheckoutConfirmation.test.ts b/tests/domain/CheckoutConfirmation.spec.ts similarity index 100% rename from tests/unit/domain/value-objects/CheckoutConfirmation.test.ts rename to tests/domain/CheckoutConfirmation.spec.ts diff --git a/tests/unit/domain/value-objects/CheckoutPrice.test.ts b/tests/domain/CheckoutPrice.spec.ts similarity index 100% rename from tests/unit/domain/value-objects/CheckoutPrice.test.ts rename to tests/domain/CheckoutPrice.spec.ts diff --git a/tests/unit/domain/value-objects/CheckoutState.test.ts b/tests/domain/CheckoutState.spec.ts similarity index 100% rename from tests/unit/domain/value-objects/CheckoutState.test.ts rename to tests/domain/CheckoutState.spec.ts diff --git a/tests/unit/domain/value-objects/CookieConfiguration.test.ts b/tests/domain/CookieConfiguration.spec.ts similarity index 100% rename from tests/unit/domain/value-objects/CookieConfiguration.test.ts rename to tests/domain/CookieConfiguration.spec.ts diff --git a/tests/unit/domain/services/DropScoreApplier.test.ts b/tests/domain/DropScoreApplier.spec.ts similarity index 100% rename from tests/unit/domain/services/DropScoreApplier.test.ts rename to tests/domain/DropScoreApplier.spec.ts diff --git a/tests/unit/domain/services/PageStateValidator.test.ts b/tests/domain/PageStateValidator.spec.ts similarity index 100% rename from tests/unit/domain/services/PageStateValidator.test.ts rename to tests/domain/PageStateValidator.spec.ts diff --git a/tests/unit/domain/value-objects/RaceCreationResult.test.ts b/tests/domain/RaceCreationResult.spec.ts similarity index 100% rename from tests/unit/domain/value-objects/RaceCreationResult.test.ts rename to tests/domain/RaceCreationResult.spec.ts diff --git a/tests/unit/domain/services/ScheduleCalculator.test.ts b/tests/domain/ScheduleCalculator.spec.ts similarity index 100% rename from tests/unit/domain/services/ScheduleCalculator.test.ts rename to tests/domain/ScheduleCalculator.spec.ts diff --git a/tests/unit/domain/value-objects/SessionLifetime.test.ts b/tests/domain/SessionLifetime.spec.ts similarity index 100% rename from tests/unit/domain/value-objects/SessionLifetime.test.ts rename to tests/domain/SessionLifetime.spec.ts diff --git a/tests/unit/domain/value-objects/SessionState.test.ts b/tests/domain/SessionState.spec.ts similarity index 100% rename from tests/unit/domain/value-objects/SessionState.test.ts rename to tests/domain/SessionState.spec.ts diff --git a/tests/unit/domain/value-objects/StepId.test.ts b/tests/domain/StepId.spec.ts similarity index 100% rename from tests/unit/domain/value-objects/StepId.test.ts rename to tests/domain/StepId.spec.ts diff --git a/tests/unit/domain/services/StepTransitionValidator.test.ts b/tests/domain/StepTransitionValidator.spec.ts similarity index 100% rename from tests/unit/domain/services/StepTransitionValidator.test.ts rename to tests/domain/StepTransitionValidator.spec.ts diff --git a/tests/unit/identity/EmailValidation.test.ts b/tests/identity/EmailValidation.spec.ts similarity index 100% rename from tests/unit/identity/EmailValidation.test.ts rename to tests/identity/EmailValidation.spec.ts diff --git a/tests/unit/infrastructure/adapters/AuthenticationGuard.test.ts b/tests/infrastructure/AuthenticationGuard.spec.ts similarity index 100% rename from tests/unit/infrastructure/adapters/AuthenticationGuard.test.ts rename to tests/infrastructure/AuthenticationGuard.spec.ts diff --git a/tests/unit/infrastructure/AutomationConfig.test.ts b/tests/infrastructure/AutomationConfig.spec.ts similarity index 100% rename from tests/unit/infrastructure/AutomationConfig.test.ts rename to tests/infrastructure/AutomationConfig.spec.ts diff --git a/tests/unit/infrastructure/config/BrowserModeConfig.test.ts b/tests/infrastructure/BrowserModeConfig.spec.ts similarity index 100% rename from tests/unit/infrastructure/config/BrowserModeConfig.test.ts rename to tests/infrastructure/BrowserModeConfig.spec.ts diff --git a/tests/unit/infrastructure/DemoImageServiceAdapter.test.ts b/tests/infrastructure/DemoImageServiceAdapter.spec.ts similarity index 100% rename from tests/unit/infrastructure/DemoImageServiceAdapter.test.ts rename to tests/infrastructure/DemoImageServiceAdapter.spec.ts diff --git a/tests/unit/infrastructure/adapters/ElectronCheckoutConfirmationAdapter.test.ts b/tests/infrastructure/ElectronCheckoutConfirmationAdapter.spec.ts similarity index 100% rename from tests/unit/infrastructure/adapters/ElectronCheckoutConfirmationAdapter.test.ts rename to tests/infrastructure/ElectronCheckoutConfirmationAdapter.spec.ts diff --git a/tests/unit/infrastructure/adapters/PlaywrightAuthSessionService.initiateLogin.browserMode.test.ts b/tests/infrastructure/PlaywrightAuthSessionService.initiateLogin.browserMode.spec.ts similarity index 100% rename from tests/unit/infrastructure/adapters/PlaywrightAuthSessionService.initiateLogin.browserMode.test.ts rename to tests/infrastructure/PlaywrightAuthSessionService.initiateLogin.browserMode.spec.ts diff --git a/tests/unit/infrastructure/adapters/PlaywrightAuthSessionService.verifyPageAuthentication.test.ts b/tests/infrastructure/PlaywrightAuthSessionService.verifyPageAuthentication.spec.ts similarity index 100% rename from tests/unit/infrastructure/adapters/PlaywrightAuthSessionService.verifyPageAuthentication.test.ts rename to tests/infrastructure/PlaywrightAuthSessionService.verifyPageAuthentication.spec.ts diff --git a/tests/unit/infrastructure/adapters/SessionCookieStore.test.ts b/tests/infrastructure/SessionCookieStore.spec.ts similarity index 100% rename from tests/unit/infrastructure/adapters/SessionCookieStore.test.ts rename to tests/infrastructure/SessionCookieStore.spec.ts diff --git a/tests/unit/infrastructure/adapters/WizardDismissalDetection.test.ts b/tests/infrastructure/WizardDismissalDetection.spec.ts similarity index 100% rename from tests/unit/infrastructure/adapters/WizardDismissalDetection.test.ts rename to tests/infrastructure/WizardDismissalDetection.spec.ts diff --git a/tests/unit/media/domain/value-objects/MediaUrl.test.ts b/tests/media/MediaUrl.spec.ts similarity index 100% rename from tests/unit/media/domain/value-objects/MediaUrl.test.ts rename to tests/media/MediaUrl.spec.ts diff --git a/tests/unit/notifications/domain/value-objects/NotificationId.test.ts b/tests/notifications/NotificationId.spec.ts similarity index 100% rename from tests/unit/notifications/domain/value-objects/NotificationId.test.ts rename to tests/notifications/NotificationId.spec.ts diff --git a/tests/unit/notifications/domain/value-objects/QuietHours.test.ts b/tests/notifications/QuietHours.spec.ts similarity index 100% rename from tests/unit/notifications/domain/value-objects/QuietHours.test.ts rename to tests/notifications/QuietHours.spec.ts diff --git a/tests/unit/racing-application/DashboardOverviewUseCase.test.ts b/tests/racing-application/DashboardOverviewUseCase.spec.ts similarity index 100% rename from tests/unit/racing-application/DashboardOverviewUseCase.test.ts rename to tests/racing-application/DashboardOverviewUseCase.spec.ts diff --git a/tests/unit/racing-application/MembershipUseCases.test.ts b/tests/racing-application/MembershipUseCases.spec.ts similarity index 100% rename from tests/unit/racing-application/MembershipUseCases.test.ts rename to tests/racing-application/MembershipUseCases.spec.ts diff --git a/tests/unit/racing-application/RaceDetailUseCases.test.ts b/tests/racing-application/RaceDetailUseCases.spec.ts similarity index 100% rename from tests/unit/racing-application/RaceDetailUseCases.test.ts rename to tests/racing-application/RaceDetailUseCases.spec.ts diff --git a/tests/unit/racing-application/RaceResultsUseCases.test.ts b/tests/racing-application/RaceResultsUseCases.spec.ts similarity index 100% rename from tests/unit/racing-application/RaceResultsUseCases.test.ts rename to tests/racing-application/RaceResultsUseCases.spec.ts diff --git a/tests/unit/racing-application/RegistrationAndTeamUseCases.test.ts b/tests/racing-application/RegistrationAndTeamUseCases.spec.ts similarity index 100% rename from tests/unit/racing-application/RegistrationAndTeamUseCases.test.ts rename to tests/racing-application/RegistrationAndTeamUseCases.spec.ts diff --git a/tests/unit/racing-application/SeasonUseCases.test.ts b/tests/racing-application/SeasonUseCases.spec.ts similarity index 100% rename from tests/unit/racing-application/SeasonUseCases.test.ts rename to tests/racing-application/SeasonUseCases.spec.ts diff --git a/tests/unit/domain/services/EventScoringService.test.ts b/tests/racing/EventScoringService.spec.ts similarity index 77% rename from tests/unit/domain/services/EventScoringService.test.ts rename to tests/racing/EventScoringService.spec.ts index 3cf23f0f1..fbd3987a3 100644 --- a/tests/unit/domain/services/EventScoringService.test.ts +++ b/tests/racing/EventScoringService.spec.ts @@ -9,65 +9,10 @@ import type { ChampionshipConfig } from '@gridpilot/racing/domain/types/Champion import { Result } from '@gridpilot/racing/domain/entities/Result'; import type { Penalty } from '@gridpilot/racing/domain/entities/Penalty'; import type { ChampionshipType } from '@gridpilot/racing/domain/types/ChampionshipType'; +import { makeDriverRef } from '../../testing/factories/racing/DriverRefFactory'; +import { makePointsTable } from '../../testing/factories/racing/PointsTableFactory'; +import { makeChampionshipConfig } from '../../testing/factories/racing/ChampionshipConfigFactory'; -function makeDriverRef(id: string): ParticipantRef { - return { - type: 'driver' as ChampionshipType, - id, - }; -} - -function makePointsTable(points: number[]): PointsTable { - const pointsByPosition: Record = {}; - points.forEach((value, index) => { - pointsByPosition[index + 1] = value; - }); - return new PointsTable(pointsByPosition); -} - -function makeChampionshipConfig(params: { - id: string; - name: string; - sessionTypes: SessionType[]; - mainPoints: number[]; - sprintPoints?: number[]; - mainBonusRules?: BonusRule[]; -}): ChampionshipConfig { - const { id, name, sessionTypes, mainPoints, sprintPoints, mainBonusRules } = params; - - const pointsTableBySessionType: Record = {} as Record; - - sessionTypes.forEach((sessionType) => { - if (sessionType === 'main') { - pointsTableBySessionType[sessionType] = makePointsTable(mainPoints); - } else if (sessionType === 'sprint' && sprintPoints) { - pointsTableBySessionType[sessionType] = makePointsTable(sprintPoints); - } else { - pointsTableBySessionType[sessionType] = new PointsTable({}); - } - }); - - const bonusRulesBySessionType: Record = {} as Record; - sessionTypes.forEach((sessionType) => { - if (sessionType === 'main' && mainBonusRules) { - bonusRulesBySessionType[sessionType] = mainBonusRules; - } else { - bonusRulesBySessionType[sessionType] = []; - } - }); - - return { - id, - name, - type: 'driver', - sessionTypes, - pointsTableBySessionType, - bonusRulesBySessionType, - dropScorePolicy: { - strategy: 'none', - }, - }; -} describe('EventScoringService', () => { const seasonId = 'season-1'; diff --git a/tests/unit/racing/domain/Season.test.ts b/tests/racing/Season.spec.ts similarity index 96% rename from tests/unit/racing/domain/Season.test.ts rename to tests/racing/Season.spec.ts index ec60a832b..370056c60 100644 --- a/tests/unit/racing/domain/Season.test.ts +++ b/tests/racing/Season.spec.ts @@ -14,16 +14,8 @@ import { type SeasonDropStrategy, } from '@gridpilot/racing/domain/value-objects/SeasonDropPolicy'; import { SeasonStewardingConfig } from '@gridpilot/racing/domain/value-objects/SeasonStewardingConfig'; +import { createMinimalSeason, createBaseSeason } from '../../testing/factories/racing/SeasonFactory'; -function createMinimalSeason(overrides?: Partial & { status?: SeasonStatus }) { - return Season.create({ - id: 'season-1', - leagueId: 'league-1', - gameId: 'iracing', - name: 'Test Season', - status: overrides?.status ?? 'planned', - }); -} describe('Season aggregate lifecycle', () => { it('transitions Planned → Active → Completed → Archived with timestamps', () => { @@ -125,18 +117,6 @@ describe('Season aggregate lifecycle', () => { }); describe('Season configuration updates', () => { - function createBaseSeason() { - return Season.create({ - id: 'season-1', - leagueId: 'league-1', - gameId: 'iracing', - name: 'Config Season', - status: 'planned', - startDate: new Date('2025-01-01T00:00:00Z'), - endDate: undefined, - maxDrivers: 24, - }); - } it('withScoringConfig returns a new Season with updated scoringConfig only', () => { const season = createBaseSeason(); diff --git a/tests/unit/structure/packages/PackageDependencies.test.ts b/tests/structure/PackageDependencies.spec.ts similarity index 100% rename from tests/unit/structure/packages/PackageDependencies.test.ts rename to tests/structure/PackageDependencies.spec.ts diff --git a/tests/unit/website/structure/AlphaComponents.test.ts b/tests/website/AlphaComponents.spec.ts similarity index 100% rename from tests/unit/website/structure/AlphaComponents.test.ts rename to tests/website/AlphaComponents.spec.ts diff --git a/tests/unit/website/structure/ImportBoundaries.test.ts b/tests/website/ImportBoundaries.spec.ts similarity index 100% rename from tests/unit/website/structure/ImportBoundaries.test.ts rename to tests/website/ImportBoundaries.spec.ts diff --git a/tests/unit/website/auth/InMemoryAuthService.test.ts b/tests/website/InMemoryAuthService.spec.ts similarity index 100% rename from tests/unit/website/auth/InMemoryAuthService.test.ts rename to tests/website/InMemoryAuthService.spec.ts diff --git a/tests/unit/website/auth/IracingAuthPageImports.test.ts b/tests/website/IracingAuthPageImports.spec.ts similarity index 100% rename from tests/unit/website/auth/IracingAuthPageImports.test.ts rename to tests/website/IracingAuthPageImports.spec.ts diff --git a/tests/unit/website/auth/IracingRoutes.test.ts b/tests/website/IracingRoutes.spec.ts similarity index 100% rename from tests/unit/website/auth/IracingRoutes.test.ts rename to tests/website/IracingRoutes.spec.ts diff --git a/tests/unit/website/getAppMode.test.ts b/tests/website/getAppMode.spec.ts similarity index 100% rename from tests/unit/website/getAppMode.test.ts rename to tests/website/getAppMode.spec.ts diff --git a/tests/unit/website/signupRoute.test.ts b/tests/website/signupRoute.spec.ts similarity index 100% rename from tests/unit/website/signupRoute.test.ts rename to tests/website/signupRoute.spec.ts