website cleanup

This commit is contained in:
2025-12-24 21:44:58 +01:00
parent 9b683a59d3
commit d78854a4c6
277 changed files with 6141 additions and 2693 deletions

View File

@@ -1,6 +1,6 @@
import { Controller, Get, Post, Body, Query } from '@nestjs/common';
import { AuthService } from './AuthService';
import { LoginParams, SignupParams, AuthSessionDTO } from './dtos/AuthDto';
import { LoginParamsDTO, SignupParamsDTO, AuthSessionDTO } from './dtos/AuthDto';
import type { CommandResultDTO } from './presenters/CommandResultPresenter';
@Controller('auth')
@@ -8,12 +8,12 @@ export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('signup')
async signup(@Body() params: SignupParams): Promise<AuthSessionDTO> {
async signup(@Body() params: SignupParamsDTO): Promise<AuthSessionDTO> {
return this.authService.signupWithEmail(params);
}
@Post('login')
async login(@Body() params: LoginParams): Promise<AuthSessionDTO> {
async login(@Body() params: LoginParamsDTO): Promise<AuthSessionDTO> {
return this.authService.loginWithEmail(params);
}

View File

@@ -26,7 +26,7 @@ import {
SIGNUP_USE_CASE_TOKEN,
} from './AuthProviders';
import type { AuthSessionDTO } from './dtos/AuthDto';
import { LoginParams, SignupParams } from './dtos/AuthDto';
import { LoginParamsDTO, SignupParamsDTO } from './dtos/AuthDto';
import { AuthSessionPresenter } from './presenters/AuthSessionPresenter';
import type { CommandResultDTO } from './presenters/CommandResultPresenter';
import { CommandResultPresenter } from './presenters/CommandResultPresenter';
@@ -67,7 +67,7 @@ export class AuthService {
};
}
async signupWithEmail(params: SignupParams): Promise<AuthSessionDTO> {
async signupWithEmail(params: SignupParamsDTO): Promise<AuthSessionDTO> {
this.logger.debug(`[AuthService] Attempting signup for email: ${params.email}`);
this.authSessionPresenter.reset();
@@ -98,7 +98,7 @@ export class AuthService {
};
}
async loginWithEmail(params: LoginParams): Promise<AuthSessionDTO> {
async loginWithEmail(params: LoginParamsDTO): Promise<AuthSessionDTO> {
this.logger.debug(`[AuthService] Attempting login for email: ${params.email}`);
this.authSessionPresenter.reset();
@@ -142,4 +142,51 @@ export class AuthService {
return this.commandResultPresenter.responseModel;
}
/**
* Start iRacing OAuth flow.
*
* NOTE: This is a placeholder implementation for the current alpha build.
* A production implementation should delegate to a dedicated iRacing OAuth port
* and persist/validate state server-side.
*/
async startIracingAuth(returnTo?: string): Promise<string> {
this.logger.debug('[AuthService] Starting iRacing auth flow', { returnTo });
const state = Math.random().toString(36).slice(2);
const base = 'https://example.com/iracing/auth';
const query = new URLSearchParams();
query.set('state', state);
if (returnTo) {
query.set('returnTo', returnTo);
}
return `${base}?${query.toString()}`;
}
/**
* Handle iRacing OAuth callback.
*
* NOTE: Placeholder implementation that creates a demo session.
*/
async iracingCallback(code: string, state: string, returnTo?: string): Promise<AuthSessionDTO> {
this.logger.debug('[AuthService] iRacing callback received', { hasCode: !!code, state, returnTo });
const userId = `iracing-${state || code}`.slice(0, 64);
const session = await this.identitySessionPort.createSession({
id: userId,
displayName: 'iRacing User',
email: '',
});
return {
token: session.token,
user: {
userId,
email: '',
displayName: 'iRacing User',
},
};
}
}

View File

@@ -16,7 +16,7 @@ export class AuthSessionDTO {
user!: AuthenticatedUserDTO;
}
export class SignupParams {
export class SignupParamsDTO {
@ApiProperty()
email!: string;
@ApiProperty()
@@ -31,21 +31,21 @@ export class SignupParams {
avatarUrl?: string;
}
export class LoginParams {
export class LoginParamsDTO {
@ApiProperty()
email!: string;
@ApiProperty()
password!: string;
}
export class IracingAuthRedirectResult {
export class IracingAuthRedirectResultDTO {
@ApiProperty()
redirectUrl!: string;
@ApiProperty()
state!: string;
}
export class LoginWithIracingCallbackParams {
export class LoginWithIracingCallbackParamsDTO {
@ApiProperty()
code!: string;
@ApiProperty()

View File

@@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsString, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
class WizardErrorsBasicsDTO {
export class WizardErrorsBasicsDTO {
@ApiProperty({ required: false })
@IsOptional()
@IsString()
@@ -19,7 +19,7 @@ class WizardErrorsBasicsDTO {
visibility?: string;
}
class WizardErrorsStructureDTO {
export class WizardErrorsStructureDTO {
@ApiProperty({ required: false })
@IsOptional()
@IsString()
@@ -36,7 +36,7 @@ class WizardErrorsStructureDTO {
driversPerTeam?: string;
}
class WizardErrorsTimingsDTO {
export class WizardErrorsTimingsDTO {
@ApiProperty({ required: false })
@IsOptional()
@IsString()
@@ -53,7 +53,7 @@ class WizardErrorsTimingsDTO {
roundsPlanned?: string;
}
class WizardErrorsScoringDTO {
export class WizardErrorsScoringDTO {
@ApiProperty({ required: false })
@IsOptional()
@IsString()
@@ -89,4 +89,4 @@ export class WizardErrorsDTO {
@IsOptional()
@IsString()
submit?: string;
}
}

View File

@@ -21,7 +21,12 @@ export class LeagueStandingsPresenter implements Presenter<GetLeagueStandingsRes
joinedAt: standing.driver.joinedAt.toString(),
},
points: standing.points,
rank: standing.rank,
position: standing.rank,
// TODO: GetLeagueStandings currently does not provide these metrics.
// Keep them stable in the API contract for now.
wins: 0,
podiums: 0,
races: 0,
}));
this.result = { standings };
}
@@ -30,4 +35,4 @@ export class LeagueStandingsPresenter implements Presenter<GetLeagueStandingsRes
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}
}

View File

@@ -1,5 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsOptional, IsUrl } from 'class-validator';
import { IsNotEmpty, IsOptional, IsString, IsUrl, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { ProtestIncidentDTO } from './ProtestIncidentDTO';
export class FileProtestCommandDTO {
@ApiProperty()
@@ -18,11 +20,9 @@ export class FileProtestCommandDTO {
accusedDriverId!: string;
@ApiProperty()
incident!: {
lap: number;
description: string;
timeInRace?: number;
};
@ValidateNested()
@Type(() => ProtestIncidentDTO)
incident!: ProtestIncidentDTO;
@ApiProperty({ required: false })
@IsOptional()
@@ -34,4 +34,4 @@ export class FileProtestCommandDTO {
@IsString()
@IsUrl()
proofVideoUrl?: string;
}
}

View File

@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator';
export class ProtestIncidentDTO {
@ApiProperty()
@IsInt()
@Min(0)
lap!: number;
@ApiProperty()
@IsString()
@IsNotEmpty()
description!: string;
@ApiProperty({ required: false, description: 'Seconds from race start' })
@IsOptional()
@IsInt()
@Min(0)
timeInRace?: number;
}

View File

@@ -18,8 +18,8 @@ import { InvoiceDTO } from './dtos/InvoiceDTO';
import { BillingStatsDTO } from './dtos/BillingStatsDTO';
import { AvailableLeagueDTO } from './dtos/AvailableLeagueDTO';
import { LeagueDetailDTO } from './dtos/LeagueDetailDTO';
import { DriverDTO } from './dtos/DriverDTO';
import { RaceDTO } from './dtos/RaceDTO';
import { SponsorDriverDTO } from './dtos/SponsorDriverDTO';
import { SponsorRaceDTO } from './dtos/RaceDTO';
import { SponsorProfileDTO } from './dtos/SponsorProfileDTO';
import { NotificationSettingsDTO } from './dtos/NotificationSettingsDTO';
import { PrivacySettingsDTO } from './dtos/PrivacySettingsDTO';
@@ -194,8 +194,8 @@ export class SponsorController {
@Param('leagueId') leagueId: string,
): Promise<{
league: LeagueDetailDTO;
drivers: DriverDTO[];
races: RaceDTO[];
drivers: SponsorDriverDTO[];
races: SponsorRaceDTO[];
} | null> {
const presenter = await this.sponsorService.getLeagueDetail(leagueId);
return presenter.viewModel;

View File

@@ -11,8 +11,8 @@ import { GetEntitySponsorshipPricingResultDTO } from './dtos/GetEntitySponsorshi
import { GetSponsorsOutputDTO } from './dtos/GetSponsorsOutputDTO';
import { AvailableLeagueDTO } from './dtos/AvailableLeagueDTO';
import { LeagueDetailDTO } from './dtos/LeagueDetailDTO';
import { DriverDTO } from './dtos/DriverDTO';
import { RaceDTO } from './dtos/RaceDTO';
import { SponsorDriverDTO } from './dtos/SponsorDriverDTO';
import { SponsorRaceDTO } from './dtos/RaceDTO';
import { SponsorProfileDTO } from './dtos/SponsorProfileDTO';
import { NotificationSettingsDTO } from './dtos/NotificationSettingsDTO';
import { PrivacySettingsDTO } from './dtos/PrivacySettingsDTO';
@@ -399,7 +399,7 @@ export class SponsorService {
},
};
const drivers: DriverDTO[] = [
const drivers: SponsorDriverDTO[] = [
{
id: 'd1',
name: 'Max Verstappen',
@@ -420,7 +420,7 @@ export class SponsorService {
},
];
const races: RaceDTO[] = [
const races: SponsorRaceDTO[] = [
{
id: 'r1',
name: 'Spa-Francorchamps',
@@ -508,4 +508,4 @@ export class SponsorService {
presenter.present({ success: true });
return presenter;
}
}
}

View File

@@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsEnum, IsNumber, IsDateString } from 'class-validator';
export class RaceDTO {
export class SponsorRaceDTO {
@ApiProperty()
@IsString()
id: string = '';
@@ -21,4 +21,4 @@ export class RaceDTO {
@ApiProperty({ enum: ['upcoming', 'completed'] })
@IsEnum(['upcoming', 'completed'])
status: 'upcoming' | 'completed' = 'upcoming';
}
}

View File

@@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNumber } from 'class-validator';
export class DriverDTO {
export class SponsorDriverDTO {
@ApiProperty()
@IsString()
id: string = '';
@@ -29,4 +29,5 @@ export class DriverDTO {
@ApiProperty()
@IsString()
team: string = '';
}
}

View File

@@ -1,11 +1,11 @@
import { LeagueDetailDTO } from '../dtos/LeagueDetailDTO';
import { DriverDTO } from '../dtos/DriverDTO';
import { RaceDTO } from '../dtos/RaceDTO';
import { SponsorDriverDTO } from '../dtos/SponsorDriverDTO';
import { SponsorRaceDTO } from '../dtos/RaceDTO';
export interface LeagueDetailViewModel {
league: LeagueDetailDTO;
drivers: DriverDTO[];
races: RaceDTO[];
drivers: SponsorDriverDTO[];
races: SponsorRaceDTO[];
}
export class LeagueDetailPresenter {

View File

@@ -1,33 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
class TeamListItemDTO {
@ApiProperty()
id!: string;
@ApiProperty()
name!: string;
@ApiProperty()
tag!: string;
@ApiProperty()
description!: string;
@ApiProperty()
memberCount!: number;
@ApiProperty({ type: [String] })
leagues!: string[];
@ApiProperty({ required: false })
specialization?: 'endurance' | 'sprint' | 'mixed';
@ApiProperty({ required: false })
region?: string;
@ApiProperty({ type: [String], required: false })
languages?: string[];
}
import { TeamListItemDTO } from './TeamListItemDTO';
export class GetAllTeamsOutputDTO {
@ApiProperty({ type: [TeamListItemDTO] })
@@ -35,4 +8,4 @@ export class GetAllTeamsOutputDTO {
@ApiProperty()
totalCount!: number;
}
}

View File

@@ -1,58 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
class TeamDTO {
@ApiProperty()
id!: string;
@ApiProperty()
name!: string;
@ApiProperty()
tag!: string;
@ApiProperty()
description!: string;
@ApiProperty()
ownerId!: string;
@ApiProperty({ type: [String] })
leagues!: string[];
@ApiProperty({ required: false })
createdAt?: string;
@ApiProperty({ required: false })
specialization?: 'endurance' | 'sprint' | 'mixed';
@ApiProperty({ required: false })
region?: string;
@ApiProperty({ type: [String], required: false })
languages?: string[];
}
class MembershipDTO {
@ApiProperty()
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt!: string;
@ApiProperty()
isActive!: boolean;
}
import { TeamDTO } from './TeamDTO';
import { TeamMembershipDTO } from './TeamMembershipDTO';
export class GetDriverTeamOutputDTO {
@ApiProperty({ type: TeamDTO })
team!: TeamDTO;
@ApiProperty({ type: MembershipDTO })
membership!: MembershipDTO;
@ApiProperty({ type: TeamMembershipDTO })
membership!: TeamMembershipDTO;
@ApiProperty()
isOwner!: boolean;
@ApiProperty()
canManage!: boolean;
}
}

View File

@@ -1,55 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
class TeamDTO {
@ApiProperty()
id!: string;
@ApiProperty()
name!: string;
@ApiProperty()
tag!: string;
@ApiProperty()
description!: string;
@ApiProperty()
ownerId!: string;
@ApiProperty({ type: [String] })
leagues!: string[];
@ApiProperty({ required: false })
createdAt?: string;
@ApiProperty({ required: false })
specialization?: 'endurance' | 'sprint' | 'mixed';
@ApiProperty({ required: false })
region?: string;
@ApiProperty({ type: [String], required: false })
languages?: string[];
}
class MembershipDTO {
@ApiProperty()
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt!: string;
@ApiProperty()
isActive!: boolean;
}
import { TeamDTO } from './TeamDTO';
import { TeamMembershipDTO } from './TeamMembershipDTO';
export class GetTeamDetailsOutputDTO {
@ApiProperty({ type: TeamDTO })
team!: TeamDTO;
@ApiProperty({ type: MembershipDTO, nullable: true })
membership!: MembershipDTO | null;
@ApiProperty({ type: TeamMembershipDTO, nullable: true })
membership!: TeamMembershipDTO | null;
@ApiProperty()
canManage!: boolean;
}
}

View File

@@ -1,27 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
class TeamJoinRequestDTO {
@ApiProperty()
requestId!: string;
@ApiProperty()
driverId!: string;
@ApiProperty()
driverName!: string;
@ApiProperty()
teamId!: string;
@ApiProperty()
status!: 'pending' | 'approved' | 'rejected';
@ApiProperty()
requestedAt!: string;
@ApiProperty()
avatarUrl!: string;
}
import { TeamJoinRequestDTO } from './TeamJoinRequestDTO';
export class GetTeamJoinRequestsOutputDTO {
@ApiProperty({ type: [TeamJoinRequestDTO] })
@@ -32,4 +11,4 @@ export class GetTeamJoinRequestsOutputDTO {
@ApiProperty()
totalCount!: number;
}
}

View File

@@ -1,24 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
class TeamMemberDTO {
@ApiProperty()
driverId!: string;
@ApiProperty()
driverName!: string;
@ApiProperty()
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt!: string;
@ApiProperty()
isActive!: boolean;
@ApiProperty()
avatarUrl!: string;
}
import { TeamMemberDTO } from './TeamMemberDTO';
export class GetTeamMembersOutputDTO {
@ApiProperty({ type: [TeamMemberDTO] })
@@ -35,4 +17,4 @@ export class GetTeamMembersOutputDTO {
@ApiProperty()
memberCount!: number;
}
}

View File

@@ -1,47 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
class TeamLeaderboardItemDTO {
@ApiProperty()
id!: string;
@ApiProperty()
name!: string;
@ApiProperty()
memberCount!: number;
@ApiProperty({ nullable: true })
rating!: number | null;
@ApiProperty()
totalWins!: number;
@ApiProperty()
totalRaces!: number;
@ApiProperty({ enum: ['beginner', 'intermediate', 'advanced', 'pro'] })
performanceLevel!: SkillLevel;
@ApiProperty()
isRecruiting!: boolean;
@ApiProperty()
createdAt!: string;
@ApiProperty({ required: false })
description?: string;
@ApiProperty({ enum: ['endurance', 'sprint', 'mixed'], required: false })
specialization?: 'endurance' | 'sprint' | 'mixed';
@ApiProperty({ required: false })
region?: string;
@ApiProperty({ type: [String], required: false })
languages?: string[];
}
import { TeamLeaderboardItemDTO, type SkillLevel } from './TeamLeaderboardItemDTO';
export class GetTeamsLeaderboardOutputDTO {
@ApiProperty({ type: [TeamLeaderboardItemDTO] })
@@ -55,4 +14,4 @@ export class GetTeamsLeaderboardOutputDTO {
@ApiProperty({ type: [TeamLeaderboardItemDTO] })
topTeams!: TeamLeaderboardItemDTO[];
}
}

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsBoolean, IsOptional } from 'class-validator';
import { IsString, IsNotEmpty, IsBoolean, IsOptional, IsArray } from 'class-validator';
export class TeamListItemViewModel {
@ApiProperty()
@@ -296,3 +296,38 @@ export class RejectTeamJoinRequestOutput {
@IsBoolean()
success!: boolean;
}
// ---
// DTOs used by the public API surface (consumed by the website via generated types)
// ---
export class TeamDTO {
@ApiProperty()
@IsString()
id!: string;
@ApiProperty()
@IsString()
name!: string;
@ApiProperty()
@IsString()
tag!: string;
@ApiProperty()
@IsString()
description!: string;
@ApiProperty()
@IsString()
ownerId!: string;
@ApiProperty({ type: [String] })
@IsArray()
leagues!: string[];
@ApiProperty({ required: false })
@IsOptional()
@IsString()
createdAt?: string;
}

View File

@@ -0,0 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
export class TeamJoinRequestDTO {
@ApiProperty()
requestId!: string;
@ApiProperty()
driverId!: string;
@ApiProperty()
driverName!: string;
@ApiProperty()
teamId!: string;
@ApiProperty({ enum: ['pending', 'approved', 'rejected'] })
status!: 'pending' | 'approved' | 'rejected';
@ApiProperty()
requestedAt!: string;
@ApiProperty()
avatarUrl!: string;
}

View File

@@ -0,0 +1,45 @@
import { ApiProperty } from '@nestjs/swagger';
export type SkillLevel = 'beginner' | 'intermediate' | 'advanced' | 'pro';
export class TeamLeaderboardItemDTO {
@ApiProperty()
id!: string;
@ApiProperty()
name!: string;
@ApiProperty()
memberCount!: number;
@ApiProperty({ nullable: true })
rating!: number | null;
@ApiProperty()
totalWins!: number;
@ApiProperty()
totalRaces!: number;
@ApiProperty({ enum: ['beginner', 'intermediate', 'advanced', 'pro'] })
performanceLevel!: SkillLevel;
@ApiProperty()
isRecruiting!: boolean;
@ApiProperty()
createdAt!: string;
@ApiProperty({ required: false })
description?: string;
@ApiProperty({ enum: ['endurance', 'sprint', 'mixed'], required: false })
specialization?: 'endurance' | 'sprint' | 'mixed';
@ApiProperty({ required: false })
region?: string;
@ApiProperty({ type: [String], required: false })
languages?: string[];
}

View File

@@ -0,0 +1,31 @@
import { ApiProperty } from '@nestjs/swagger';
export class TeamListItemDTO {
@ApiProperty()
id!: string;
@ApiProperty()
name!: string;
@ApiProperty()
tag!: string;
@ApiProperty()
description!: string;
@ApiProperty()
memberCount!: number;
@ApiProperty({ type: [String] })
leagues!: string[];
@ApiProperty({ required: false, enum: ['endurance', 'sprint', 'mixed'] })
specialization?: 'endurance' | 'sprint' | 'mixed';
@ApiProperty({ required: false })
region?: string;
@ApiProperty({ type: [String], required: false })
languages?: string[];
}

View File

@@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
export class TeamMemberDTO {
@ApiProperty()
driverId!: string;
@ApiProperty()
driverName!: string;
@ApiProperty({ enum: ['owner', 'manager', 'member'] })
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt!: string;
@ApiProperty()
isActive!: boolean;
@ApiProperty()
avatarUrl!: string;
}

View File

@@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
export class TeamMembershipDTO {
@ApiProperty({ enum: ['owner', 'manager', 'member'] })
role!: 'owner' | 'manager' | 'member';
@ApiProperty()
joinedAt!: string;
@ApiProperty()
isActive!: boolean;
}