This commit is contained in:
2025-12-04 11:54:23 +01:00
parent c0fdae3d3c
commit 9d5caa87f3
83 changed files with 1579 additions and 2151 deletions

View File

@@ -0,0 +1,121 @@
'use client';
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from 'react';
import { useRouter } from 'next/navigation';
import type { AuthSession } from './AuthService';
type AuthContextValue = {
session: AuthSession | null;
loading: boolean;
login: (returnTo?: string) => void;
logout: () => Promise<void>;
};
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
interface AuthProviderProps {
initialSession?: AuthSession | null;
children: ReactNode;
}
export function AuthProvider({ initialSession = null, children }: AuthProviderProps) {
const router = useRouter();
const [session, setSession] = useState<AuthSession | null>(initialSession);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (initialSession) return;
let cancelled = false;
async function loadSession() {
try {
const res = await fetch('/api/auth/session', {
method: 'GET',
credentials: 'include',
});
if (!res.ok) {
if (!cancelled) setSession(null);
return;
}
const data = (await res.json()) as { session: AuthSession | null };
if (!cancelled) {
setSession(data.session ?? null);
}
} catch {
if (!cancelled) {
setSession(null);
}
}
}
loadSession();
return () => {
cancelled = true;
};
}, [initialSession]);
const login = useCallback(
(returnTo?: string) => {
const search = new URLSearchParams();
if (returnTo) {
search.set('returnTo', returnTo);
}
const target = search.toString()
? `/auth/iracing?${search.toString()}`
: '/auth/iracing';
router.push(target);
},
[router],
);
const logout = useCallback(async () => {
setLoading(true);
try {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
setSession(null);
router.push('/');
router.refresh();
} finally {
setLoading(false);
}
}, [router]);
const value = useMemo(
() => ({
session,
loading,
login,
logout,
}),
[session, loading, login, logout],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) {
throw new Error('useAuth must be used within an AuthProvider');
}
return ctx;
}

View File

@@ -0,0 +1,27 @@
export interface AuthUser {
id: string;
displayName: string;
iracingCustomerId?: string;
primaryDriverId?: string;
avatarUrl?: string;
}
export interface AuthSession {
user: AuthUser;
issuedAt: number;
expiresAt: number;
token: string;
}
export interface AuthService {
getCurrentSession(): Promise<AuthSession | null>;
startIracingAuthRedirect(
returnTo?: string,
): Promise<{ redirectUrl: string; state: string }>;
loginWithIracingCallback(params: {
code: string;
state: string;
returnTo?: string;
}): Promise<AuthSession>;
logout(): Promise<void>;
}

View File

@@ -0,0 +1,96 @@
import { cookies } from 'next/headers';
import { randomUUID } from 'crypto';
import type { AuthService, AuthSession, AuthUser } from './AuthService';
import { createStaticRacingSeed } from '@gridpilot/testing-support';
const SESSION_COOKIE = 'gp_demo_session';
const STATE_COOKIE = 'gp_demo_auth_state';
function parseCookieValue(raw: string | undefined): AuthSession | null {
if (!raw) return null;
try {
const parsed = JSON.parse(raw) as AuthSession;
if (!parsed.expiresAt || Date.now() > parsed.expiresAt) {
return null;
}
return parsed;
} catch {
return null;
}
}
function serializeSession(session: AuthSession): string {
return JSON.stringify(session);
}
export class InMemoryAuthService implements AuthService {
private readonly seedDriverId: string;
constructor() {
const seed = createStaticRacingSeed(42);
this.seedDriverId = seed.drivers[0]?.id ?? 'driver-1';
}
async getCurrentSession(): Promise<AuthSession | null> {
const store = await cookies();
const raw = store.get(SESSION_COOKIE)?.value;
return parseCookieValue(raw);
}
async startIracingAuthRedirect(
returnTo?: string,
): Promise<{ redirectUrl: string; state: string }> {
const state = randomUUID();
const params = new URLSearchParams();
params.set('code', 'dummy-code');
params.set('state', state);
if (returnTo) {
params.set('returnTo', returnTo);
}
return {
redirectUrl: `/auth/iracing/callback?${params.toString()}`,
state,
};
}
async loginWithIracingCallback(params: {
code: string;
state: string;
returnTo?: string;
}): Promise<AuthSession> {
if (!params.code) {
throw new Error('Missing auth code');
}
if (!params.state) {
throw new Error('Missing auth state');
}
const user: AuthUser = {
id: 'demo-user',
displayName: 'GridPilot Demo Driver',
iracingCustomerId: '000000',
primaryDriverId: this.seedDriverId,
avatarUrl: `/api/avatar/${this.seedDriverId}`,
};
const now = Date.now();
const expiresAt = now + 24 * 60 * 60 * 1000;
const session: AuthSession = {
user,
issuedAt: now,
expiresAt,
token: randomUUID(),
};
return session;
}
async logout(): Promise<void> {
// Intentionally does nothing; cookie deletion is handled by route handlers.
return;
}
}

View File

@@ -0,0 +1,11 @@
import type { AuthService } from './AuthService';
import { InMemoryAuthService } from './InMemoryAuthService';
let authService: AuthService | null = null;
export function getAuthService(): AuthService {
if (!authService) {
authService = new InMemoryAuthService();
}
return authService;
}

View File

@@ -1,27 +1,34 @@
/**
* Dependency Injection Container
*
*
* Initializes all in-memory repositories and provides accessor functions.
* Allows easy swapping to persistent repositories later.
*/
import { Driver } from '@gridpilot/racing-domain/entities/Driver';
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 { Driver } from '@gridpilot/racing/domain/entities/Driver';
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 type { IDriverRepository } from '@gridpilot/racing-domain/ports/IDriverRepository';
import type { ILeagueRepository } from '@gridpilot/racing-domain/ports/ILeagueRepository';
import type { IRaceRepository } from '@gridpilot/racing-domain/ports/IRaceRepository';
import type { IResultRepository } from '@gridpilot/racing-domain/ports/IResultRepository';
import type { IStandingRepository } from '@gridpilot/racing-domain/ports/IStandingRepository';
import type { IDriverRepository } from '@gridpilot/racing/domain/repositories/IDriverRepository';
import type { ILeagueRepository } from '@gridpilot/racing/domain/repositories/ILeagueRepository';
import type { IRaceRepository } from '@gridpilot/racing/domain/repositories/IRaceRepository';
import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IResultRepository';
import type { IStandingRepository } from '@gridpilot/racing/domain/repositories/IStandingRepository';
import type { IFeedRepository } from '@gridpilot/social/domain/repositories/IFeedRepository';
import type { ISocialGraphRepository } from '@gridpilot/social/domain/repositories/ISocialGraphRepository';
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 { InMemoryResultRepository } from '@gridpilot/racing-infrastructure/repositories/InMemoryResultRepository';
import { InMemoryStandingRepository } from '@gridpilot/racing-infrastructure/repositories/InMemoryStandingRepository';
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 { InMemoryResultRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryResultRepository';
import { InMemoryStandingRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryStandingRepository';
import { createStaticRacingSeed, type RacingSeedData } from '@gridpilot/testing-support';
import {
InMemoryFeedRepository,
InMemorySocialGraphRepository,
} from '@gridpilot/social/infrastructure/inmemory/InMemorySocialAndFeed';
/**
* Seed data for development
@@ -49,193 +56,34 @@ export interface DriverStats {
*/
const driverStats: Record<string, DriverStats> = {};
function createSeedData() {
// Create sample drivers (matching membership-data.ts and team-data.ts)
const driver1 = Driver.create({
id: 'driver-1',
iracingId: '123456',
name: 'Max Verstappen',
country: 'NL',
bio: 'Three-time world champion and team owner of Apex Racing',
joinedAt: new Date('2024-01-15'),
function createSeedData(): RacingSeedData {
const seed = createStaticRacingSeed(42);
const { drivers } = seed;
drivers.forEach((driver, index) => {
const totalRaces = 40 + index * 5;
const wins = Math.max(0, Math.floor(totalRaces * 0.2) - index);
const podiums = Math.max(wins * 2, 0);
const dnfs = Math.max(0, Math.floor(index / 2));
const rating = 1500 + index * 25;
driverStats[driver.id] = {
driverId: driver.id,
rating,
totalRaces,
wins,
podiums,
dnfs,
avgFinish: 4,
bestFinish: 1,
worstFinish: 20,
consistency: 80,
overallRank: index + 1,
percentile: Math.max(0, 100 - index),
};
});
const driver2 = Driver.create({
id: 'driver-2',
iracingId: '234567',
name: 'Lewis Hamilton',
country: 'GB',
bio: 'Seven-time world champion leading Speed Demons',
joinedAt: new Date('2024-01-20'),
});
const driver3 = Driver.create({
id: 'driver-3',
iracingId: '345678',
name: 'Charles Leclerc',
country: 'MC',
bio: 'Ferrari race winner and Weekend Warriors team owner',
joinedAt: new Date('2024-02-01'),
});
const driver4 = Driver.create({
id: 'driver-4',
iracingId: '456789',
name: 'Lando Norris',
country: 'GB',
bio: 'Rising star in motorsport',
joinedAt: new Date('2024-02-15'),
});
// Initialize driver stats
driverStats['driver-1'] = {
driverId: 'driver-1',
rating: 3245,
totalRaces: 156,
wins: 45,
podiums: 89,
dnfs: 8,
avgFinish: 3.2,
bestFinish: 1,
worstFinish: 18,
consistency: 87,
overallRank: 1,
percentile: 99
};
driverStats['driver-2'] = {
driverId: 'driver-2',
rating: 3198,
totalRaces: 234,
wins: 78,
podiums: 145,
dnfs: 12,
avgFinish: 2.8,
bestFinish: 1,
worstFinish: 22,
consistency: 92,
overallRank: 2,
percentile: 98
};
driverStats['driver-3'] = {
driverId: 'driver-3',
rating: 2912,
totalRaces: 145,
wins: 34,
podiums: 67,
dnfs: 9,
avgFinish: 4.5,
bestFinish: 1,
worstFinish: 20,
consistency: 84,
overallRank: 3,
percentile: 96
};
driverStats['driver-4'] = {
driverId: 'driver-4',
rating: 2789,
totalRaces: 112,
wins: 23,
podiums: 56,
dnfs: 7,
avgFinish: 5.1,
bestFinish: 1,
worstFinish: 16,
consistency: 81,
overallRank: 5,
percentile: 93
};
// Create sample league (matching membership-data.ts)
const league1 = League.create({
id: 'league-1',
name: 'European GT Championship',
description: 'Weekly GT3 racing with professional drivers',
ownerId: driver1.id,
settings: {
pointsSystem: 'f1-2024',
sessionDuration: 60,
qualifyingFormat: 'open',
},
createdAt: new Date('2024-01-20'),
});
// Create sample races
const race1 = Race.create({
id: 'race-1',
leagueId: league1.id,
scheduledAt: new Date('2024-03-15T19:00:00Z'),
track: 'Monza GP',
car: 'Porsche 911 GT3 R',
sessionType: 'race',
status: 'completed',
});
const race2 = Race.create({
id: 'race-2',
leagueId: league1.id,
scheduledAt: new Date('2024-03-22T19:00:00Z'),
track: 'Spa-Francorchamps',
car: 'Porsche 911 GT3 R',
sessionType: 'race',
status: 'scheduled',
});
const race3 = Race.create({
id: 'race-3',
leagueId: league1.id,
scheduledAt: new Date('2024-04-05T19:00:00Z'),
track: 'Nürburgring GP',
car: 'Porsche 911 GT3 R',
sessionType: 'race',
status: 'scheduled',
});
// Create sample standings
const standing1 = Standing.create({
leagueId: league1.id,
driverId: driver1.id,
position: 1,
points: 25,
wins: 1,
racesCompleted: 1,
});
const standing2 = Standing.create({
leagueId: league1.id,
driverId: driver2.id,
position: 2,
points: 18,
wins: 0,
racesCompleted: 1,
});
const standing3 = Standing.create({
leagueId: league1.id,
driverId: driver3.id,
position: 3,
points: 15,
wins: 0,
racesCompleted: 1,
});
const standing4 = Standing.create({
leagueId: league1.id,
driverId: driver4.id,
position: 4,
points: 12,
wins: 0,
racesCompleted: 1,
});
return {
drivers: [driver1, driver2, driver3, driver4],
leagues: [league1],
races: [race1, race2, race3],
standings: [standing1, standing2, standing3, standing4],
};
return seed;
}
/**
@@ -249,6 +97,8 @@ class DIContainer {
private _raceRepository: IRaceRepository;
private _resultRepository: IResultRepository;
private _standingRepository: IStandingRepository;
private _feedRepository: IFeedRepository;
private _socialRepository: ISocialGraphRepository;
private constructor() {
// Create seed data
@@ -261,7 +111,7 @@ class DIContainer {
// Result repository needs race repository for league-based queries
this._resultRepository = new InMemoryResultRepository(
undefined,
seedData.results,
this._raceRepository
);
@@ -272,6 +122,10 @@ class DIContainer {
this._raceRepository,
this._leagueRepository
);
// Social and feed adapters backed by static seed
this._feedRepository = new InMemoryFeedRepository(seedData);
this._socialRepository = new InMemorySocialGraphRepository(seedData);
}
/**
@@ -313,6 +167,14 @@ class DIContainer {
get standingRepository(): IStandingRepository {
return this._standingRepository;
}
get feedRepository(): IFeedRepository {
return this._feedRepository;
}
get socialRepository(): ISocialGraphRepository {
return this._socialRepository;
}
}
/**
@@ -338,6 +200,14 @@ export function getStandingRepository(): IStandingRepository {
return DIContainer.getInstance().standingRepository;
}
export function getFeedRepository(): IFeedRepository {
return DIContainer.getInstance().feedRepository;
}
export function getSocialRepository(): ISocialGraphRepository {
return DIContainer.getInstance().socialRepository;
}
/**
* Reset function for testing
*/

View File

@@ -1,53 +0,0 @@
import { z } from 'zod';
/**
* Email validation schema using Zod
*/
export const emailSchema = z.string()
.email('Invalid email format')
.min(3, 'Email too short')
.max(254, 'Email too long')
.toLowerCase()
.trim();
/**
* Validates an email address
* @param email - The email address to validate
* @returns Validation result with sanitized email or error
*/
export function validateEmail(email: string): {
success: boolean;
email?: string;
error?: string;
} {
const result = emailSchema.safeParse(email);
if (result.success) {
return {
success: true,
email: result.data,
};
}
return {
success: false,
error: result.error.errors[0]?.message || 'Invalid email',
};
}
/**
* Check if email appears to be from a disposable email service
* Basic check - can be extended with comprehensive list
*/
const DISPOSABLE_DOMAINS = new Set([
'tempmail.com',
'throwaway.email',
'guerrillamail.com',
'mailinator.com',
'10minutemail.com',
]);
export function isDisposableEmail(email: string): boolean {
const domain = email.split('@')[1]?.toLowerCase();
return domain ? DISPOSABLE_DOMAINS.has(domain) : false;
}

View File

@@ -1,208 +0,0 @@
/**
* In-memory league membership data for alpha prototype
*/
export type MembershipRole = 'owner' | 'admin' | 'steward' | 'member';
export type MembershipStatus = 'active' | 'pending' | 'none';
export interface LeagueMembership {
leagueId: string;
driverId: string;
role: MembershipRole;
status: MembershipStatus;
joinedAt: Date;
}
export interface JoinRequest {
id: string;
leagueId: string;
driverId: string;
requestedAt: Date;
message?: string;
}
// In-memory storage
let memberships: LeagueMembership[] = [];
let joinRequests: JoinRequest[] = [];
// Current driver ID (matches the one in di-container)
const CURRENT_DRIVER_ID = 'driver-1';
// Initialize with seed data
export function initializeMembershipData() {
memberships = [
{
leagueId: 'league-1',
driverId: CURRENT_DRIVER_ID,
role: 'owner',
status: 'active',
joinedAt: new Date('2024-01-15'),
},
{
leagueId: 'league-1',
driverId: 'driver-2',
role: 'member',
status: 'active',
joinedAt: new Date('2024-02-01'),
},
{
leagueId: 'league-1',
driverId: 'driver-3',
role: 'admin',
status: 'active',
joinedAt: new Date('2024-02-15'),
},
];
joinRequests = [];
}
// Get membership for a driver in a league
export function getMembership(leagueId: string, driverId: string): LeagueMembership | null {
return memberships.find(m => m.leagueId === leagueId && m.driverId === driverId) || null;
}
// Get all members for a league
export function getLeagueMembers(leagueId: string): LeagueMembership[] {
return memberships.filter(m => m.leagueId === leagueId && m.status === 'active');
}
// Get pending join requests for a league
export function getJoinRequests(leagueId: string): JoinRequest[] {
return joinRequests.filter(r => r.leagueId === leagueId);
}
// Join a league
export function joinLeague(leagueId: string, driverId: string): void {
const existing = getMembership(leagueId, driverId);
if (existing) {
throw new Error('Already a member or have a pending request');
}
memberships.push({
leagueId,
driverId,
role: 'member',
status: 'active',
joinedAt: new Date(),
});
}
// Request to join a league (for invite-only leagues)
export function requestToJoin(leagueId: string, driverId: string, message?: string): void {
const existing = getMembership(leagueId, driverId);
if (existing) {
throw new Error('Already a member or have a pending request');
}
const existingRequest = joinRequests.find(r => r.leagueId === leagueId && r.driverId === driverId);
if (existingRequest) {
throw new Error('Join request already pending');
}
joinRequests.push({
id: `request-${Date.now()}`,
leagueId,
driverId,
requestedAt: new Date(),
message,
});
}
// Leave a league
export function leaveLeague(leagueId: string, driverId: string): void {
const membership = getMembership(leagueId, driverId);
if (!membership) {
throw new Error('Not a member of this league');
}
if (membership.role === 'owner') {
throw new Error('League owner cannot leave. Transfer ownership first.');
}
memberships = memberships.filter(m => !(m.leagueId === leagueId && m.driverId === driverId));
}
// Approve join request
export function approveJoinRequest(requestId: string): void {
const request = joinRequests.find(r => r.id === requestId);
if (!request) {
throw new Error('Join request not found');
}
memberships.push({
leagueId: request.leagueId,
driverId: request.driverId,
role: 'member',
status: 'active',
joinedAt: new Date(),
});
joinRequests = joinRequests.filter(r => r.id !== requestId);
}
// Reject join request
export function rejectJoinRequest(requestId: string): void {
joinRequests = joinRequests.filter(r => r.id !== requestId);
}
// Remove member (admin action)
export function removeMember(leagueId: string, driverId: string, removedBy: string): void {
const removerMembership = getMembership(leagueId, removedBy);
if (!removerMembership || (removerMembership.role !== 'owner' && removerMembership.role !== 'admin')) {
throw new Error('Only owners and admins can remove members');
}
const targetMembership = getMembership(leagueId, driverId);
if (!targetMembership) {
throw new Error('Member not found');
}
if (targetMembership.role === 'owner') {
throw new Error('Cannot remove league owner');
}
memberships = memberships.filter(m => !(m.leagueId === leagueId && m.driverId === driverId));
}
// Update member role
export function updateMemberRole(
leagueId: string,
driverId: string,
newRole: MembershipRole,
updatedBy: string
): void {
const updaterMembership = getMembership(leagueId, updatedBy);
if (!updaterMembership || updaterMembership.role !== 'owner') {
throw new Error('Only league owner can change roles');
}
const targetMembership = getMembership(leagueId, driverId);
if (!targetMembership) {
throw new Error('Member not found');
}
if (newRole === 'owner') {
throw new Error('Use transfer ownership to change owner');
}
memberships = memberships.map(m =>
m.leagueId === leagueId && m.driverId === driverId
? { ...m, role: newRole }
: m
);
}
// Check if driver is owner or admin
export function isOwnerOrAdmin(leagueId: string, driverId: string): boolean {
const membership = getMembership(leagueId, driverId);
return membership?.role === 'owner' || membership?.role === 'admin';
}
// Get current driver ID
export function getCurrentDriverId(): string {
return CURRENT_DRIVER_ID;
}
// Initialize on module load
initializeMembershipData();

View File

@@ -1,130 +0,0 @@
/**
* In-memory race registration data for alpha prototype
*/
import { getMembership } from './membership-data';
export interface RaceRegistration {
raceId: string;
driverId: string;
registeredAt: Date;
}
// In-memory storage (Set for quick lookups)
const registrations = new Map<string, Set<string>>(); // raceId -> Set of driverIds
/**
* Generate registration key for storage
*/
function getRegistrationKey(raceId: string, driverId: string): string {
return `${raceId}:${driverId}`;
}
/**
* Check if driver is registered for a race
*/
export function isRegistered(raceId: string, driverId: string): boolean {
const raceRegistrations = registrations.get(raceId);
return raceRegistrations ? raceRegistrations.has(driverId) : false;
}
/**
* Get all registered drivers for a race
*/
export function getRegisteredDrivers(raceId: string): string[] {
const raceRegistrations = registrations.get(raceId);
return raceRegistrations ? Array.from(raceRegistrations) : [];
}
/**
* Get registration count for a race
*/
export function getRegistrationCount(raceId: string): number {
const raceRegistrations = registrations.get(raceId);
return raceRegistrations ? raceRegistrations.size : 0;
}
/**
* Register driver for a race
* Validates league membership before registering
*/
export function registerForRace(
raceId: string,
driverId: string,
leagueId: string
): void {
// Check if already registered
if (isRegistered(raceId, driverId)) {
throw new Error('Already registered for this race');
}
// Validate league membership
const membership = getMembership(leagueId, driverId);
if (!membership || membership.status !== 'active') {
throw new Error('Must be an active league member to register for races');
}
// Add registration
if (!registrations.has(raceId)) {
registrations.set(raceId, new Set());
}
registrations.get(raceId)!.add(driverId);
}
/**
* Withdraw from a race
*/
export function withdrawFromRace(raceId: string, driverId: string): void {
const raceRegistrations = registrations.get(raceId);
if (!raceRegistrations || !raceRegistrations.has(driverId)) {
throw new Error('Not registered for this race');
}
raceRegistrations.delete(driverId);
// Clean up empty sets
if (raceRegistrations.size === 0) {
registrations.delete(raceId);
}
}
/**
* Get all races a driver is registered for
*/
export function getDriverRegistrations(driverId: string): string[] {
const raceIds: string[] = [];
for (const [raceId, driverSet] of registrations.entries()) {
if (driverSet.has(driverId)) {
raceIds.push(raceId);
}
}
return raceIds;
}
/**
* Clear all registrations for a race (e.g., when race is cancelled)
*/
export function clearRaceRegistrations(raceId: string): void {
registrations.delete(raceId);
}
/**
* Initialize with seed data
*/
export function initializeRegistrationData(): void {
registrations.clear();
// Add some initial registrations for testing
// Race 2 (Spa-Francorchamps - upcoming)
registerForRace('race-2', 'driver-1', 'league-1');
registerForRace('race-2', 'driver-2', 'league-1');
registerForRace('race-2', 'driver-3', 'league-1');
// Race 3 (Nürburgring GP - upcoming)
registerForRace('race-3', 'driver-1', 'league-1');
}
// Initialize on module load
initializeRegistrationData();

View File

@@ -1,335 +0,0 @@
/**
* In-memory team data for alpha prototype
*/
export type TeamRole = 'owner' | 'manager' | 'driver';
export type TeamMembershipStatus = 'active' | 'pending' | 'none';
export interface Team {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
leagues: string[];
createdAt: Date;
}
export interface TeamMembership {
teamId: string;
driverId: string;
role: TeamRole;
status: TeamMembershipStatus;
joinedAt: Date;
}
export interface TeamJoinRequest {
id: string;
teamId: string;
driverId: string;
requestedAt: Date;
message?: string;
}
// In-memory storage
let teams: Team[] = [];
let teamMemberships: TeamMembership[] = [];
let teamJoinRequests: TeamJoinRequest[] = [];
// Current driver ID (matches di-container)
const CURRENT_DRIVER_ID = 'driver-1';
// Initialize with seed data
export function initializeTeamData() {
teams = [
{
id: 'team-1',
name: 'Apex Racing',
tag: 'APEX',
description: 'Professional GT3 racing team competing at the highest level',
ownerId: CURRENT_DRIVER_ID,
leagues: ['league-1'],
createdAt: new Date('2024-01-20'),
},
{
id: 'team-2',
name: 'Speed Demons',
tag: 'SPDM',
description: 'Fast and furious racing with a competitive edge',
ownerId: 'driver-2',
leagues: ['league-1'],
createdAt: new Date('2024-02-01'),
},
{
id: 'team-3',
name: 'Weekend Warriors',
tag: 'WKND',
description: 'Casual but competitive weekend racing',
ownerId: 'driver-3',
leagues: ['league-1'],
createdAt: new Date('2024-02-10'),
},
];
teamMemberships = [
{
teamId: 'team-1',
driverId: CURRENT_DRIVER_ID,
role: 'owner',
status: 'active',
joinedAt: new Date('2024-01-20'),
},
{
teamId: 'team-2',
driverId: 'driver-2',
role: 'owner',
status: 'active',
joinedAt: new Date('2024-02-01'),
},
{
teamId: 'team-3',
driverId: 'driver-3',
role: 'owner',
status: 'active',
joinedAt: new Date('2024-02-10'),
},
];
teamJoinRequests = [];
}
// Get all teams
export function getAllTeams(): Team[] {
return teams;
}
// Get team by ID
export function getTeam(teamId: string): Team | null {
return teams.find(t => t.id === teamId) || null;
}
// Get team membership for a driver
export function getTeamMembership(teamId: string, driverId: string): TeamMembership | null {
return teamMemberships.find(m => m.teamId === teamId && m.driverId === driverId) || null;
}
// Get driver's team
export function getDriverTeam(driverId: string): { team: Team; membership: TeamMembership } | null {
const membership = teamMemberships.find(m => m.driverId === driverId && m.status === 'active');
if (!membership) return null;
const team = getTeam(membership.teamId);
if (!team) return null;
return { team, membership };
}
// Get all members for a team
export function getTeamMembers(teamId: string): TeamMembership[] {
return teamMemberships.filter(m => m.teamId === teamId && m.status === 'active');
}
// Get pending join requests for a team
export function getTeamJoinRequests(teamId: string): TeamJoinRequest[] {
return teamJoinRequests.filter(r => r.teamId === teamId);
}
// Create a new team
export function createTeam(
name: string,
tag: string,
description: string,
ownerId: string,
leagues: string[]
): Team {
// Check if driver already has a team
const existingTeam = getDriverTeam(ownerId);
if (existingTeam) {
throw new Error('Driver already belongs to a team');
}
const team: Team = {
id: `team-${Date.now()}`,
name,
tag,
description,
ownerId,
leagues,
createdAt: new Date(),
};
teams.push(team);
// Auto-assign creator as owner
teamMemberships.push({
teamId: team.id,
driverId: ownerId,
role: 'owner',
status: 'active',
joinedAt: new Date(),
});
return team;
}
// Join a team
export function joinTeam(teamId: string, driverId: string): void {
const existingTeam = getDriverTeam(driverId);
if (existingTeam) {
throw new Error('Driver already belongs to a team');
}
const existing = getTeamMembership(teamId, driverId);
if (existing) {
throw new Error('Already a member or have a pending request');
}
teamMemberships.push({
teamId,
driverId,
role: 'driver',
status: 'active',
joinedAt: new Date(),
});
}
// Request to join a team
export function requestToJoinTeam(teamId: string, driverId: string, message?: string): void {
const existingTeam = getDriverTeam(driverId);
if (existingTeam) {
throw new Error('Driver already belongs to a team');
}
const existing = getTeamMembership(teamId, driverId);
if (existing) {
throw new Error('Already a member or have a pending request');
}
const existingRequest = teamJoinRequests.find(r => r.teamId === teamId && r.driverId === driverId);
if (existingRequest) {
throw new Error('Join request already pending');
}
teamJoinRequests.push({
id: `team-request-${Date.now()}`,
teamId,
driverId,
requestedAt: new Date(),
message,
});
}
// Leave a team
export function leaveTeam(teamId: string, driverId: string): void {
const membership = getTeamMembership(teamId, driverId);
if (!membership) {
throw new Error('Not a member of this team');
}
if (membership.role === 'owner') {
throw new Error('Team owner cannot leave. Transfer ownership or disband team first.');
}
teamMemberships = teamMemberships.filter(m => !(m.teamId === teamId && m.driverId === driverId));
}
// Approve join request
export function approveTeamJoinRequest(requestId: string): void {
const request = teamJoinRequests.find(r => r.id === requestId);
if (!request) {
throw new Error('Join request not found');
}
teamMemberships.push({
teamId: request.teamId,
driverId: request.driverId,
role: 'driver',
status: 'active',
joinedAt: new Date(),
});
teamJoinRequests = teamJoinRequests.filter(r => r.id !== requestId);
}
// Reject join request
export function rejectTeamJoinRequest(requestId: string): void {
teamJoinRequests = teamJoinRequests.filter(r => r.id !== requestId);
}
// Remove member (admin action)
export function removeTeamMember(teamId: string, driverId: string, removedBy: string): void {
const removerMembership = getTeamMembership(teamId, removedBy);
if (!removerMembership || (removerMembership.role !== 'owner' && removerMembership.role !== 'manager')) {
throw new Error('Only owners and managers can remove members');
}
const targetMembership = getTeamMembership(teamId, driverId);
if (!targetMembership) {
throw new Error('Member not found');
}
if (targetMembership.role === 'owner') {
throw new Error('Cannot remove team owner');
}
teamMemberships = teamMemberships.filter(m => !(m.teamId === teamId && m.driverId === driverId));
}
// Update member role
export function updateTeamMemberRole(
teamId: string,
driverId: string,
newRole: TeamRole,
updatedBy: string
): void {
const updaterMembership = getTeamMembership(teamId, updatedBy);
if (!updaterMembership || updaterMembership.role !== 'owner') {
throw new Error('Only team owner can change roles');
}
const targetMembership = getTeamMembership(teamId, driverId);
if (!targetMembership) {
throw new Error('Member not found');
}
if (newRole === 'owner') {
throw new Error('Use transfer ownership to change owner');
}
teamMemberships = teamMemberships.map(m =>
m.teamId === teamId && m.driverId === driverId
? { ...m, role: newRole }
: m
);
}
// Check if driver is owner or manager
export function isTeamOwnerOrManager(teamId: string, driverId: string): boolean {
const membership = getTeamMembership(teamId, driverId);
return membership?.role === 'owner' || membership?.role === 'manager';
}
// Get current driver ID
export function getCurrentDriverId(): string {
return CURRENT_DRIVER_ID;
}
// Update team info
export function updateTeam(
teamId: string,
updates: Partial<Pick<Team, 'name' | 'tag' | 'description' | 'leagues'>>,
updatedBy: string
): void {
if (!isTeamOwnerOrManager(teamId, updatedBy)) {
throw new Error('Only owners and managers can update team info');
}
teams = teams.map(t =>
t.id === teamId
? { ...t, ...updates }
: t
);
}
// Initialize on module load
initializeTeamData();