Files
gridpilot.gg/adapters/bootstrap/SeedDemoUsers.ts
2026-01-16 21:57:44 +01:00

266 lines
8.7 KiB
TypeScript

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 { UserRepository } from '@core/identity/domain/repositories/UserRepository';
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: false,
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 the email as the seed for the primary driver ID
const seedKey = `primary-driver-${email}`;
return this.generateDeterministicId(seedKey, persistence);
}
async execute(): Promise<void> {
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<void> {
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(', ')}`);
}
}
}
}