Backend Complete: Interface Refactoring & Service Container Enhancements
Repository Interface Optimization: - Created IBaseRepository.ts and IPaginatedRepository.ts - Refactored all 7 repository interfaces to extend base interfaces - Eliminated ~200 lines of redundant code (70% reduction) - Improved type safety and maintainability Dependency Injection Improvements: - Added EmailService and GameTokenService to DIContainer - Updated CreateUserCommandHandler constructor for DI - Updated RequestPasswordResetCommandHandler constructor for DI - Enhanced testability and service consistency Environment Configuration: - Created comprehensive .env.example with 40+ variables - Organized into 12 logical sections (Database, Security, Email, etc.) - Added security guidelines and best practices - Documented all backend environment requirements Documentation: - Added comprehensive codebase review - Created refactoring summary report - Added frontend implementation guide Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
This commit is contained in:
@@ -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,91 @@
|
||||
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.APP_BASE_URL || 'http://localhost:3000';
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,196 @@
|
||||
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 });
|
||||
throw new Error('Invalid username');
|
||||
}
|
||||
|
||||
// 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 = '';
|
||||
let errorMessage = '';
|
||||
switch (user.state) {
|
||||
case UserState.REGISTERED_NOT_VERIFIED:
|
||||
stateDescription = 'Email not verified';
|
||||
errorMessage = 'User account not verified';
|
||||
break;
|
||||
case UserState.SOFT_DELETE:
|
||||
stateDescription = 'Account deleted';
|
||||
errorMessage = 'User account deactivated';
|
||||
break;
|
||||
case UserState.DEACTIVATED:
|
||||
stateDescription = 'Account deactivated';
|
||||
errorMessage = 'User account deactivated';
|
||||
break;
|
||||
}
|
||||
|
||||
logAuth('Login failed - Account state restriction', user.id, {
|
||||
username: cmd.username,
|
||||
userState: user.state,
|
||||
stateDescription
|
||||
});
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
throw new Error('Invalid password');
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Password verification error', error as Error);
|
||||
throw new Error('Invalid password');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// Re-throw authentication/validation errors as-is
|
||||
if (error.message.includes('Invalid username') ||
|
||||
error.message.includes('Invalid password') ||
|
||||
error.message.includes('not verified') ||
|
||||
error.message.includes('deactivated') ||
|
||||
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,145 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { logAuth, logError, logWarning } from '../../Services/Logger';
|
||||
import { IUserRepository } from '../../../Domain/IRepository/IUserRepository';
|
||||
import { JWTService } from '../../Services/JWTService';
|
||||
import { RedisService } from '../../Services/RedisService';
|
||||
|
||||
export class LogoutCommandHandler {
|
||||
private jwtService: JWTService;
|
||||
private redisService: RedisService;
|
||||
|
||||
constructor(private readonly userRepo: IUserRepository) {
|
||||
this.jwtService = new JWTService();
|
||||
this.redisService = RedisService.getInstance();
|
||||
}
|
||||
|
||||
async execute(userId: string, res: Response, req?: Request): Promise<boolean> {
|
||||
try {
|
||||
logAuth('Logout process started', userId);
|
||||
|
||||
// 1. Get token from request to blacklist it
|
||||
let tokenToBlacklist: string | null = null;
|
||||
if (req) {
|
||||
// Extract token from cookie
|
||||
tokenToBlacklist = req.cookies['auth_token'];
|
||||
|
||||
// Also check Authorization header as fallback
|
||||
if (!tokenToBlacklist && req.headers.authorization) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader.startsWith('Bearer ')) {
|
||||
tokenToBlacklist = authHeader.substring(7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Blacklist the current JWT token in Redis (if available)
|
||||
if (tokenToBlacklist && req) {
|
||||
try {
|
||||
// Store token in blacklist with expiration matching token expiry
|
||||
const decoded = this.jwtService.verify(req);
|
||||
if (decoded && decoded.exp) {
|
||||
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
|
||||
if (ttl > 0) {
|
||||
await this.redisService.setWithExpiry(`blacklist:${tokenToBlacklist}`, 'true', ttl);
|
||||
logAuth('JWT token blacklisted', userId, { tokenExpiry: ttl });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logWarning('Failed to blacklist token', { userId, error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Clear authentication cookie
|
||||
res.clearCookie('auth_token', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
path: '/'
|
||||
});
|
||||
|
||||
// 4. Remove user from active sessions in Redis
|
||||
try {
|
||||
await this.redisService.removeActiveUser(userId);
|
||||
logAuth('User removed from active sessions', userId);
|
||||
} catch (error) {
|
||||
logWarning('Failed to remove user from active sessions', { userId, error: (error as Error).message });
|
||||
// Continue even if this fails
|
||||
}
|
||||
|
||||
// 5. Update user's last logout timestamp in database
|
||||
try {
|
||||
const updateResult = await this.userRepo.update(userId, { updatedate: new Date() });
|
||||
if (updateResult) {
|
||||
logAuth('User last logout timestamp updated', userId);
|
||||
}
|
||||
} catch (error) {
|
||||
logWarning('Failed to update user logout timestamp', { userId, error: (error as Error).message });
|
||||
// Continue even if this fails
|
||||
}
|
||||
|
||||
// 6. Clear any user-specific cache entries
|
||||
try {
|
||||
// Clear user session data
|
||||
await this.redisService.del(`user:${userId}:session`);
|
||||
await this.redisService.del(`user:${userId}:active_chats`);
|
||||
logAuth('User cache cleared', userId);
|
||||
} catch (error) {
|
||||
logWarning('Failed to clear user cache', { userId, error: (error as Error).message });
|
||||
// Continue even if this fails
|
||||
}
|
||||
|
||||
logAuth('User logout completed successfully', userId);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
logError('LogoutCommandHandler error', error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a token is blacklisted
|
||||
*/
|
||||
async isTokenBlacklisted(token: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.redisService.get(`blacklist:${token}`);
|
||||
return result === 'true';
|
||||
} catch (error) {
|
||||
logError('Error checking token blacklist', error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user from all devices by blacklisting all their active tokens
|
||||
* This is a simplified version - in a real implementation you'd track active tokens per user
|
||||
*/
|
||||
async logoutFromAllDevices(userId: string): Promise<boolean> {
|
||||
try {
|
||||
// Clear all user-related Redis keys
|
||||
const userKeys = [
|
||||
`user:${userId}:session`,
|
||||
`user:${userId}:active_chats`,
|
||||
`user:${userId}:active_tokens`,
|
||||
`user:${userId}:websocket_connections`
|
||||
];
|
||||
|
||||
for (const key of userKeys) {
|
||||
try {
|
||||
await this.redisService.del(key);
|
||||
} catch (error) {
|
||||
logWarning(`Failed to delete Redis key: ${key}`, { userId, error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
// Update user logout timestamp
|
||||
await this.userRepo.update(userId, { updatedate: new Date() });
|
||||
|
||||
logAuth('User logged out from all devices', userId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logError('Error logging out user from all devices', error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface RequestPasswordResetCommand {
|
||||
email: string;
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
|
||||
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 {
|
||||
constructor(
|
||||
private userRepo: IUserRepository,
|
||||
private emailService: 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 { DetailUserDto } 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<DetailUserDto | null> {
|
||||
try {
|
||||
const user = await this.userRepo.findById(query.id);
|
||||
if (!user) return null;
|
||||
return UserMapper.toDetailDto(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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user