import { AdminUser } from '@core/admin/domain/entities/AdminUser'; import { Email } from '@core/admin/domain/value-objects/Email'; import type { AdminUserRepository } from '@core/admin/domain/repositories/AdminUserRepository'; import { User } from '@core/identity/domain/entities/User'; import type { AuthRepository } from '@core/identity/domain/repositories/AuthRepository'; import type { PasswordHashingService } from '@core/identity/domain/services/PasswordHashingService'; import { EmailAddress } from '@core/identity/domain/value-objects/EmailAddress'; import { PasswordHash } from '@core/identity/domain/value-objects/PasswordHash'; import { UserId } from '@core/identity/domain/value-objects/UserId'; import type { Logger } from '@core/shared/domain/Logger'; import { stableUuidFromSeedKey } from './racing/SeedIdHelper'; interface DemoUserSpec { email: string; password: string; needsAdminUser: boolean; needsPrimaryDriverId: boolean; roles: string[]; displayName: string; } /** * SeedDemoUsers - Creates predefined demo users for testing and development * * This class creates a canonical set of demo users with fixed emails and passwords. * It is idempotent and supports force reseeding via environment variables. */ export class SeedDemoUsers { private readonly demoUserSpecs: DemoUserSpec[] = [ { email: 'demo.driver@example.com', password: 'Demo1234!', needsAdminUser: false, needsPrimaryDriverId: true, roles: ['user'], displayName: 'John Driver', }, { email: 'demo.sponsor@example.com', password: 'Demo1234!', needsAdminUser: false, needsPrimaryDriverId: true, roles: ['sponsor'], displayName: 'Jane Sponsor', }, { email: 'demo.owner@example.com', password: 'Demo1234!', needsAdminUser: true, needsPrimaryDriverId: true, roles: ['owner'], displayName: 'Alice Owner', }, { email: 'demo.steward@example.com', password: 'Demo1234!', needsAdminUser: true, needsPrimaryDriverId: true, roles: ['user'], displayName: 'Bob Steward', }, { email: 'demo.admin@example.com', password: 'Demo1234!', needsAdminUser: true, needsPrimaryDriverId: true, roles: ['admin'], displayName: 'Charlie Admin', }, { email: 'demo.systemowner@example.com', password: 'Demo1234!', needsAdminUser: true, needsPrimaryDriverId: true, roles: ['admin'], displayName: 'Diana SystemOwner', }, { email: 'demo.superadmin@example.com', password: 'Demo1234!', needsAdminUser: true, needsPrimaryDriverId: true, roles: ['admin'], displayName: 'Edward SuperAdmin', }, ]; constructor( private readonly logger: Logger, private readonly authRepository: AuthRepository, private readonly passwordHashingService: PasswordHashingService, private readonly adminUserRepository: AdminUserRepository, ) {} private getApiPersistence(): 'postgres' | 'inmemory' { const configured = process.env.GRIDPILOT_API_PERSISTENCE?.toLowerCase(); if (configured === 'postgres' || configured === 'inmemory') { return configured; } if (process.env.NODE_ENV === 'test') { return 'inmemory'; } return process.env.DATABASE_URL ? 'postgres' : 'inmemory'; } private generateDeterministicId(seedKey: string, persistence: 'postgres' | 'inmemory'): string { if (persistence === 'postgres') { return stableUuidFromSeedKey(seedKey); } return seedKey; } private generatePrimaryDriverId(email: string, persistence: 'postgres' | 'inmemory'): string { // Use predefined IDs for demo users to match SeedRacingData const demoDriverIds: Record = { 'demo.driver@example.com': 'driver-1', 'demo.sponsor@example.com': 'driver-2', 'demo.owner@example.com': 'driver-3', 'demo.steward@example.com': 'driver-4', 'demo.admin@example.com': 'driver-5', 'demo.systemowner@example.com': 'driver-6', 'demo.superadmin@example.com': 'driver-7', }; const seedKey = demoDriverIds[email] || `primary-driver-${email}`; return this.generateDeterministicId(seedKey, persistence); } async execute(): Promise { const persistence = this.getApiPersistence(); // Check for force reseed via environment variable const forceReseedRaw = process.env.GRIDPILOT_API_FORCE_RESEED; const forceReseed = forceReseedRaw !== undefined && forceReseedRaw !== '0' && forceReseedRaw.toLowerCase() !== 'false'; this.logger.info( `[Bootstrap] Demo users seed precheck: forceReseed=${forceReseed}, persistence=${persistence}`, ); // Check if all demo users already exist let allUsersExist = true; for (const spec of this.demoUserSpecs) { const existingUser = await this.authRepository.findByEmail(EmailAddress.create(spec.email)); if (!existingUser) { allUsersExist = false; break; } // Also check for admin users if needed if (spec.needsAdminUser) { const existingAdmin = await this.adminUserRepository.findByEmail(Email.create(spec.email)); if (!existingAdmin) { allUsersExist = false; break; } } } if (allUsersExist && !forceReseed) { this.logger.info('[Bootstrap] Demo users already exist, skipping'); return; } if (forceReseed) { this.logger.info('[Bootstrap] Force reseed enabled - updating existing demo users'); } else { this.logger.info('[Bootstrap] Starting demo users seed'); } // Create or update each demo user for (const spec of this.demoUserSpecs) { await this.createOrUpdateDemoUser(spec, persistence); } this.logger.info( `[Bootstrap] Demo users seed completed: ${this.demoUserSpecs.length} users processed`, ); } private async createOrUpdateDemoUser(spec: DemoUserSpec, persistence: 'postgres' | 'inmemory'): Promise { const userId = this.generateDeterministicId(`demo-user-${spec.email}`, persistence); // Check if user exists const existingUser = await this.authRepository.findByEmail(EmailAddress.create(spec.email)); // Hash the password const passwordHash = await this.passwordHashingService.hash(spec.password); if (existingUser) { // Update existing user const rehydrateProps: { id: string; displayName: string; email?: string; passwordHash?: PasswordHash; primaryDriverId?: string; } = { id: existingUser.getId().value, displayName: spec.displayName, email: spec.email, passwordHash: PasswordHash.fromHash(passwordHash), }; if (spec.needsPrimaryDriverId) { rehydrateProps.primaryDriverId = this.generatePrimaryDriverId(spec.email, persistence); } const updatedUser = User.rehydrate(rehydrateProps); await this.authRepository.save(updatedUser); this.logger.debug(`[Bootstrap] Updated demo user: ${spec.email}`); } else { // Create new user const createProps: { id: UserId; displayName: string; email?: string; passwordHash?: PasswordHash; primaryDriverId?: string; } = { id: UserId.fromString(userId), displayName: spec.displayName, email: spec.email, passwordHash: PasswordHash.fromHash(passwordHash), }; if (spec.needsPrimaryDriverId) { createProps.primaryDriverId = this.generatePrimaryDriverId(spec.email, persistence); } const user = User.create(createProps); await this.authRepository.save(user); this.logger.debug(`[Bootstrap] Created demo user: ${spec.email}`); } // Handle admin user if needed if (spec.needsAdminUser) { const adminUserId = this.generateDeterministicId(`demo-admin-${spec.email}`, persistence); const existingAdmin = await this.adminUserRepository.findByEmail(Email.create(spec.email)); if (existingAdmin) { // Admin user exists, no update needed for now this.logger.debug(`[Bootstrap] Admin user already exists: ${spec.email}`); } else { // Create admin user const adminCreateProps: { id: string; email: string; roles: string[]; status: string; displayName: string; primaryDriverId?: string; } = { id: adminUserId, email: spec.email, roles: spec.roles, status: 'active', displayName: spec.displayName, }; if (spec.needsPrimaryDriverId) { adminCreateProps.primaryDriverId = this.generatePrimaryDriverId(spec.email, persistence); } const adminUser = AdminUser.create(adminCreateProps); await this.adminUserRepository.create(adminUser); this.logger.debug(`[Bootstrap] Created admin user: ${spec.email} with roles: ${spec.roles.join(', ')}`); } } } }