265 lines
8.6 KiB
TypeScript
265 lines
8.6 KiB
TypeScript
import type { Logger } from '@core/shared/application';
|
|
import type { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository';
|
|
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
|
|
import type { IAdminUserRepository } from '@core/admin/domain/repositories/IAdminUserRepository';
|
|
import { User } from '@core/identity/domain/entities/User';
|
|
import { AdminUser } from '@core/admin/domain/entities/AdminUser';
|
|
import { EmailAddress } from '@core/identity/domain/value-objects/EmailAddress';
|
|
import { UserId } from '@core/identity/domain/value-objects/UserId';
|
|
import { Email } from '@core/admin/domain/value-objects/Email';
|
|
import { PasswordHash } from '@core/identity/domain/value-objects/PasswordHash';
|
|
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: true,
|
|
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: IAuthRepository,
|
|
private readonly passwordHashingService: IPasswordHashingService,
|
|
private readonly adminUserRepository: IAdminUserRepository,
|
|
) {}
|
|
|
|
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(', ')}`);
|
|
}
|
|
}
|
|
}
|
|
} |