Files
SerpentRace/SerpentRace_Backend/src/Application/User/commands/CreateUserCommandHandler.ts
T
2025-10-24 21:16:23 +02:00

92 lines
3.6 KiB
TypeScript

import { IUserRepository } from '../../../Domain/IRepository/IUserRepository';
import { CreateUserCommand } from './CreateUserCommand';
import { ShortUserDto } from '../../DTOs/UserDto';
import { UserAggregate, UserState } from '../../../Domain/User/UserAggregate';
import { UserMapper } from '../../DTOs/Mappers/UserMapper';
import { PasswordService } from '../../Services/PasswordService';
import { EmailService } from '../../Services/EmailService';
import { TokenService } from '../../Services/TokenService';
import { logDatabase, logError, logAuth, logWarning } from '../../Services/Logger';
export class CreateUserCommandHandler {
constructor(
private readonly userRepo: IUserRepository,
private readonly emailService: EmailService
) {}
async execute(cmd: CreateUserCommand): Promise<ShortUserDto> {
try {
// Validate password strength
const passwordValidation = PasswordService.validatePasswordStrength(cmd.password);
if (!passwordValidation.isValid) {
throw new Error(`Password validation failed: ${passwordValidation.errors.join(', ')}`);
}
const user = new UserAggregate();
user.username = cmd.username;
// Hash the password before storing
user.password = await PasswordService.hashPassword(cmd.password);
// Generate verification token
const verificationTokenData = TokenService.generateVerificationToken();
user.token = await TokenService.hashToken(verificationTokenData.token);
user.TokenExpires = verificationTokenData.expiresAt;
user.email = cmd.email;
user.fname = cmd.fname;
user.lname = cmd.lname;
user.orgid = cmd.orgid || null;
user.phone = cmd.phone || null;
user.state = UserState.REGISTERED_NOT_VERIFIED;
const created = await this.userRepo.create(user);
// Send verification email (non-blocking)
this.sendVerificationEmailAsync(created, verificationTokenData.token);
return UserMapper.toShortDto(created);
} catch (error) {
// Only log the error once here, don't log again in router
const errorMessage = (error as Error).message;
// Re-throw validation errors as-is (don't log as these are user input errors)
if (errorMessage.includes('Password validation failed')) {
throw error;
}
// Handle database constraint errors
if (errorMessage.includes('duplicate') || errorMessage.includes('unique') ||
errorMessage.includes('UNIQUE constraint') || errorMessage.includes('already exists')) {
throw new Error('User with this username or email already exists');
}
// Log database/system errors but throw user-friendly message
logError('CreateUserCommandHandler error', error as Error);
throw new Error('Failed to create user');
}
}
private async sendVerificationEmailAsync(user: UserAggregate, token: string): Promise<void> {
try {
const baseUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
const verificationUrl = TokenService.generateVerificationUrl(baseUrl, token);
const emailSent = await this.emailService.sendVerificationEmail(
user.email,
`${user.fname} ${user.lname}`,
token,
verificationUrl
);
if (!emailSent) {
logWarning('Failed to send verification email', { email: user.email, userId: user.id });
} else {
logAuth('Verification email sent successfully', user.id, { email: user.email });
}
} catch (emailError) {
logError('Error sending verification email', emailError as Error);
}
}
}