92 lines
3.6 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|