https://project.mdnd-it.cc/work_packages/94
This commit is contained in:
2025-08-23 04:25:28 +02:00
parent 725516ad6c
commit 19cfa031d0
25823 changed files with 1095587 additions and 2801760 deletions
@@ -0,0 +1,11 @@
export interface CreateUserCommand {
username: string;
password: string;
email: string;
fname: string;
lname: string;
code?: string;
orgid?: string;
type: string;
phone?: string;
}
@@ -0,0 +1,89 @@
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 {
private emailService: EmailService;
constructor(private readonly userRepo: IUserRepository) {
this.emailService = new 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.token = cmd.code || null;
user.type = cmd.type;
user.phone = cmd.phone || null;
user.state = UserState.REGISTERED_NOT_VERIFIED;
const created = await this.userRepo.create(user);
// Send verification email
try {
const baseUrl = process.env.APP_BASE_URL || 'http://localhost:3000';
const verificationUrl = TokenService.generateVerificationUrl(baseUrl, verificationTokenData.token);
const emailSent = await this.emailService.sendVerificationEmail(
created.email,
`${created.fname} ${created.lname}`,
verificationTokenData.token,
verificationUrl
);
if (!emailSent) {
logWarning('Failed to send verification email', { email: created.email, userId: created.id });
// Don't throw error - user creation should still succeed even if email fails
} else {
logAuth('Verification email sent successfully', created.id, { email: created.email });
}
} catch (emailError) {
logError('Error sending verification email', emailError as Error);
// Don't throw error - user creation should still succeed even if email fails
}
return UserMapper.toShortDto(created);
} catch (error) {
logError('CreateUserCommandHandler error', error as Error);
// Re-throw validation errors as-is
if (error instanceof Error && error.message.includes('Password validation failed')) {
throw error;
}
// Handle database constraint errors
if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique'))) {
throw new Error('User with this username or email already exists');
}
// Generic error for other cases
throw new Error('Failed to create user');
}
}
}
@@ -0,0 +1,3 @@
export interface DeactivateUserCommand {
id: string;
}
@@ -0,0 +1,12 @@
import { IUserRepository } from '../../../Domain/IRepository/IUserRepository';
import { DeactivateUserCommand } from './DeactivateUserCommand';
export class DeactivateUserCommandHandler {
constructor(private readonly userRepo: IUserRepository) {}
async execute(cmd: DeactivateUserCommand): Promise<boolean> {
await this.userRepo.deactivate(cmd.id);
return true;
}
}
@@ -0,0 +1,4 @@
export interface DeleteUserCommand {
id: string;
soft?: boolean;
}
@@ -0,0 +1,16 @@
import { IUserRepository } from '../../../Domain/IRepository/IUserRepository';
import { DeleteUserCommand } from './DeleteUserCommand';
export class DeleteUserCommandHandler {
constructor(private readonly userRepo: IUserRepository) {}
async execute(cmd: DeleteUserCommand): Promise<boolean> {
if (cmd.soft) {
await this.userRepo.softDelete(cmd.id);
} else {
await this.userRepo.delete(cmd.id);
}
return true;
}
}
@@ -0,0 +1,4 @@
export interface LoginCommand {
username: string;
password: string;
}
@@ -0,0 +1,188 @@
import { IUserRepository } from '../../../Domain/IRepository/IUserRepository';
import { IOrganizationRepository } from '../../../Domain/IRepository/IOrganizationRepository';
import { LoginCommand } from './LoginCommand';
import { ShortUserDto } from '../../DTOs/UserDto';
import { UserMapper } from '../../DTOs/Mappers/UserMapper';
import { PasswordService } from '../../Services/PasswordService';
import { JWTService } from '../../Services/JWTService';
import { UserState } from '../../../Domain/User/UserAggregate';
import { logAuth, logDatabase, logError, logWarning } from '../../Services/Logger';
import { Response } from 'express';
export interface LoginResponse {
user: ShortUserDto;
token: string;
requiresOrgReauth?: boolean;
orgLoginUrl?: string;
organizationName?: string;
}
export class LoginCommandHandler {
constructor(
private readonly userRepo: IUserRepository,
private readonly jwtService: JWTService,
private readonly orgRepo: IOrganizationRepository
) {}
async execute(cmd: LoginCommand, res?: Response): Promise<LoginResponse | null> {
const startTime = Date.now();
try {
logAuth('Login attempt', undefined, { username: cmd.username });
const user = await this.userRepo.findByUsername(cmd.username) ||
await this.userRepo.findByEmail(cmd.username);
logDatabase('User lookup completed', undefined, Date.now() - startTime, {
found: !!user,
searchBy: cmd.username.includes('@') ? 'email' : 'username'
});
if (!user) {
logAuth('Login failed - User not found', undefined, { username: cmd.username });
return null;
}
// Check if user account state allows login
const restrictedStates = [
UserState.REGISTERED_NOT_VERIFIED,
UserState.SOFT_DELETE,
UserState.DEACTIVATED
];
if (restrictedStates.includes(user.state)) {
let stateDescription = '';
switch (user.state) {
case UserState.REGISTERED_NOT_VERIFIED:
stateDescription = 'Email not verified';
break;
case UserState.SOFT_DELETE:
stateDescription = 'Account deleted';
break;
case UserState.DEACTIVATED:
stateDescription = 'Account deactivated';
break;
}
logAuth('Login failed - Account state restriction', user.id, {
username: cmd.username,
userState: user.state,
stateDescription
});
return null;
}
try {
const passwordStartTime = Date.now();
const isPasswordValid = await PasswordService.verifyPassword(cmd.password, user.password);
logAuth('Password verification completed', user.id, {
valid: isPasswordValid,
verificationTime: Date.now() - passwordStartTime
});
if (!isPasswordValid) {
logWarning('Login failed - Invalid password', {
userId: user.id,
username: cmd.username
});
return null;
}
} catch (error) {
logError('Password verification error', error as Error);
return null;
}
const mockRes = {
cookie: () => {}
} as any;
const tokenPayload = {
userId: user.id,
authLevel: (user.state === UserState.ADMIN ? 1 : 0) as 0 | 1,
userStatus: user.state,
orgId: user.orgid || ''
};
try {
// Use the real response object if provided, otherwise use mock
const responseObj = res || mockRes;
const token = this.jwtService.create(tokenPayload, responseObj);
// Check if user belongs to an organization and needs reauthentication
let requiresOrgReauth = false;
let orgLoginUrl: string | undefined;
let organizationName: string | undefined;
if (user.orgid) {
const organization = await this.orgRepo.findById(user.orgid);
if (organization) {
organizationName = organization.name;
// Check if user has logged in to organization within the last month
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
const needsReauth = !user.Orglogindate || user.Orglogindate < oneMonthAgo;
if (needsReauth && organization.url) {
requiresOrgReauth = true;
orgLoginUrl = organization.url;
logAuth('User requires organization reauthentication', user.id, {
organizationId: user.orgid,
organizationName: organization.name,
lastOrgLogin: user.Orglogindate?.toISOString() || 'never',
orgLoginUrl: organization.url
});
}
}
}
logAuth('Login successful', user.id, {
authLevel: tokenPayload.authLevel,
userStatus: tokenPayload.userStatus,
orgId: tokenPayload.orgId,
requiresOrgReauth,
organizationName,
totalLoginTime: Date.now() - startTime
});
const response: LoginResponse = {
user: UserMapper.toShortDto(user),
token
};
if (requiresOrgReauth) {
response.requiresOrgReauth = true;
response.orgLoginUrl = orgLoginUrl;
response.organizationName = organizationName;
}
return response;
} catch (error) {
logError('Token creation failed during login', error as Error);
throw new Error('Login failed due to internal error');
}
} catch (error) {
if (error instanceof Error) {
logError('Login handler error', error);
// Handle database connection errors
if (error.message.includes('database connection')) {
logDatabase('Database connection error during login', undefined, Date.now() - startTime);
throw new Error('Database connection error');
}
// If it's already a properly formatted error, re-throw it
if (error.message === 'Login failed due to internal error' ||
error.message === 'Database connection error') {
throw error;
}
}
// Default database error handling
logDatabase('Unexpected database error during login', undefined, Date.now() - startTime);
throw new Error('Database connection error');
}
}
}
@@ -0,0 +1,3 @@
export interface RequestPasswordResetCommand {
email: string;
}
@@ -0,0 +1,69 @@
import { RequestPasswordResetCommand } from './RequestPasswordResetCommand';
import { IUserRepository } from '../../../Domain/IRepository/IUserRepository';
import { EmailService } from '../../Services/EmailService';
import { TokenService } from '../../Services/TokenService';
import { logAuth, logWarning, logError } from '../../Services/Logger';
export class RequestPasswordResetCommandHandler {
private emailService: EmailService;
constructor(private userRepo: IUserRepository) {
this.emailService = new EmailService();
}
async execute(cmd: RequestPasswordResetCommand): Promise<boolean> {
try {
if (!cmd.email) {
throw new Error('Email is required');
}
// Find user by email
const user = await this.userRepo.findByEmail(cmd.email);
if (!user) {
// Don't reveal if user exists or not for security reasons
// Still return true but don't send email
logAuth(`Password reset requested for non-existent email: ${cmd.email}`);
return true;
}
// Generate password reset token
const resetTokenData = TokenService.generatePasswordResetToken();
// Update user with reset token
user.token = await TokenService.hashToken(resetTokenData.token);
user.TokenExpires = resetTokenData.expiresAt;
await this.userRepo.update(user.id, user);
// Send password reset email
try {
const baseUrl = process.env.APP_BASE_URL || 'http://localhost:3000';
const resetUrl = TokenService.generatePasswordResetUrl(baseUrl, resetTokenData.token);
const emailSent = await this.emailService.sendPasswordResetEmail(
user.email,
`${user.fname} ${user.lname}`,
resetTokenData.token,
resetUrl
);
if (!emailSent) {
logWarning(`Failed to send password reset email to ${user.email}`);
// Don't throw error - request should still succeed even if email fails
} else {
logAuth(`Password reset email sent successfully to ${user.email}`);
}
} catch (emailError) {
logError('Error sending password reset email', emailError instanceof Error ? emailError : new Error(String(emailError)));
// Don't throw error - request should still succeed even if email fails
}
return true;
} catch (error) {
logError('Password reset request error', error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
}
@@ -0,0 +1,4 @@
export interface ResetPasswordCommand {
token: string;
newPassword: string;
}
@@ -0,0 +1,58 @@
import { ResetPasswordCommand } from './ResetPasswordCommand';
import { IUserRepository } from '../../../Domain/IRepository/IUserRepository';
import { TokenService } from '../../Services/TokenService';
import { PasswordService } from '../../Services/PasswordService';
import { logError } from '../../Services/Logger';
export class ResetPasswordCommandHandler {
constructor(private userRepo: IUserRepository) {}
async execute(cmd: ResetPasswordCommand): Promise<boolean> {
try {
if (!cmd.token) {
throw new Error('Reset token is required');
}
if (!cmd.newPassword) {
throw new Error('New password is required');
}
// Validate password strength
const validation = PasswordService.validatePasswordStrength(cmd.newPassword);
if (!validation.isValid) {
throw new Error(`Password validation failed: ${validation.errors.join(', ')}`);
}
// Hash the token to compare with stored value
const hashedToken = await TokenService.hashToken(cmd.token);
// Find user with this password reset token
const user = await this.userRepo.findByToken(hashedToken);
if (!user) {
throw new Error('Invalid or expired reset token');
}
// Check if token is expired
if (user.TokenExpires && user.TokenExpires < new Date()) {
throw new Error('Reset token has expired');
}
// Hash the new password
const hashedPassword = await PasswordService.hashPassword(cmd.newPassword);
// Update user password and clear reset token
user.password = hashedPassword;
user.token = null;
user.TokenExpires = null;
await this.userRepo.update(user.id, user);
return true;
} catch (error) {
logError('Password reset error', error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
}
@@ -0,0 +1,13 @@
export interface UpdateUserCommand {
id: string;
orgid?: string;
username?: string;
password?: string;
email?: string;
fname?: string;
lname?: string;
code?: string;
type?: string;
phone?: string;
state?: number;
}
@@ -0,0 +1,29 @@
import { IUserRepository } from '../../../Domain/IRepository/IUserRepository';
import { UpdateUserCommand } from './UpdateUserCommand';
import { ShortUserDto } from '../../DTOs/UserDto';
import { UserMapper } from '../../DTOs/Mappers/UserMapper';
import { PasswordService } from '../../Services/PasswordService';
export class UpdateUserCommandHandler {
constructor(private readonly userRepo: IUserRepository) {}
async execute(cmd: UpdateUserCommand): Promise<ShortUserDto | null> {
const updateData = { ...cmd };
// Hash the password if it's being updated
if (cmd.password) {
// Validate password strength
const passwordValidation = PasswordService.validatePasswordStrength(cmd.password);
if (!passwordValidation.isValid) {
throw new Error(`Password validation failed: ${passwordValidation.errors.join(', ')}`);
}
updateData.password = await PasswordService.hashPassword(cmd.password);
}
const updated = await this.userRepo.update(cmd.id, updateData);
if (!updated) return null;
return UserMapper.toShortDto(updated);
}
}
@@ -0,0 +1,3 @@
export interface VerifyEmailCommand {
token: string;
}
@@ -0,0 +1,45 @@
import { VerifyEmailCommand } from './VerifyEmailCommand';
import { IUserRepository } from '../../../Domain/IRepository/IUserRepository';
import { TokenService } from '../../Services/TokenService';
import { UserState } from '../../../Domain/User/UserAggregate';
import { logError } from '../../Services/Logger';
export class VerifyEmailCommandHandler {
constructor(private userRepo: IUserRepository) {}
async execute(cmd: VerifyEmailCommand): Promise<boolean> {
try {
if (!cmd.token) {
throw new Error('Verification token is required');
}
// Hash the token to compare with stored value
const hashedToken = await TokenService.hashToken(cmd.token);
// Find user with this verification token
const user = await this.userRepo.findByToken(hashedToken);
if (!user) {
throw new Error('Invalid or expired verification token');
}
// Check if token is expired
if (user.TokenExpires && user.TokenExpires < new Date()) {
throw new Error('Verification token has expired');
}
// Update user verification status
user.token = null;
user.TokenExpires = null;
user.state = UserState.VERIFIED_REGULAR;
await this.userRepo.update(user.id, user);
return true;
} catch (error) {
logError('Email verification error', error instanceof Error ? error : new Error(String(error)));
throw error;
}
}
}
@@ -0,0 +1,3 @@
export interface GetUserByIdQuery {
id: string;
}
@@ -0,0 +1,32 @@
import { IUserRepository } from '../../../Domain/IRepository/IUserRepository';
import { GetUserByIdQuery } from './GetUserByIdQuery';
import { ShortUserDto } from '../../DTOs/UserDto';
import { UserMapper } from '../../DTOs/Mappers/UserMapper';
import { logError } from '../../Services/Logger';
export class GetUserByIdQueryHandler {
constructor(private readonly userRepo: IUserRepository) {}
async execute(query: GetUserByIdQuery): Promise<ShortUserDto | null> {
try {
const user = await this.userRepo.findById(query.id);
if (!user) return null;
return UserMapper.toShortDto(user);
} catch (error) {
logError('GetUserByIdQueryHandler error', error instanceof Error ? error : new Error(String(error)));
// Handle invalid ID format
if (error instanceof Error && error.message.includes('invalid') && error.message.includes('uuid')) {
return null; // Treat invalid UUID as not found
}
// Handle database errors
if (error instanceof Error && error.message.includes('database')) {
throw new Error('Database connection error');
}
// Generic error for other cases
throw new Error('Failed to retrieve user');
}
}
}
@@ -0,0 +1,5 @@
export interface GetUsersByPageQuery {
from: number;
to: number;
includeDeleted?: boolean;
}
@@ -0,0 +1,60 @@
import { IUserRepository } from '../../../Domain/IRepository/IUserRepository';
import { GetUsersByPageQuery } from './GetUsersByPageQuery';
import { ShortUserDto } from '../../DTOs/UserDto';
import { UserMapper } from '../../DTOs/Mappers/UserMapper';
import { logError, logRequest } from '../../Services/Logger';
export class GetUsersByPageQueryHandler {
constructor(private readonly userRepo: IUserRepository) {}
async execute(query: GetUsersByPageQuery): Promise<{ users: ShortUserDto[], totalCount: number }> {
try {
// Validate pagination parameters
if (query.from < 0 || query.to < query.from) {
throw new Error('Invalid pagination parameters');
}
const limit = query.to - query.from + 1;
if (limit > 100) {
throw new Error('Page size too large. Maximum 100 records per request');
}
logRequest('Get users by page query started', undefined, undefined, {
from: query.from,
to: query.to,
includeDeleted: query.includeDeleted || false
});
const result = query.includeDeleted
? await this.userRepo.findByPageIncludingDeleted(query.from, query.to)
: await this.userRepo.findByPage(query.from, query.to);
logRequest('Get users by page query completed', undefined, undefined, {
from: query.from,
to: query.to,
returned: result.users.length,
totalCount: result.totalCount,
includeDeleted: query.includeDeleted || false
});
return {
users: UserMapper.toShortDtoList(result.users),
totalCount: result.totalCount
};
} catch (error) {
logError('GetUsersByPageQueryHandler error', error instanceof Error ? error : new Error(String(error)));
// Handle database errors
if (error instanceof Error && error.message.includes('database')) {
throw new Error('Database connection error');
}
// Re-throw validation errors as-is
if (error instanceof Error && (error.message.includes('Invalid pagination') || error.message.includes('Page size'))) {
throw error;
}
throw new Error('Failed to retrieve users');
}
}
}