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:
2025-09-21 03:27:57 +02:00
parent 5b7c3ba4b2
commit 86211923db
306 changed files with 52956 additions and 0 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,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;
}
@@ -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');
}
}
}