fel kesz game backend

This commit is contained in:
2025-09-15 19:00:35 +02:00
parent 7963f28021
commit 3af8de2797
267 changed files with 15655 additions and 347 deletions
@@ -38,52 +38,55 @@ export class CreateUserCommandHandler {
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
}
// Send verification email (non-blocking)
this.sendVerificationEmailAsync(created, verificationTokenData.token);
return UserMapper.toShortDto(created);
} catch (error) {
logError('CreateUserCommandHandler error', error as 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
if (error instanceof Error && error.message.includes('Password validation failed')) {
// 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 (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique'))) {
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');
}
// Generic error for other cases
// 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);
}
}
}
@@ -40,7 +40,7 @@ export class LoginCommandHandler {
if (!user) {
logAuth('Login failed - User not found', undefined, { username: cmd.username });
return null;
throw new Error('Invalid username');
}
// Check if user account state allows login
@@ -52,15 +52,19 @@ export class LoginCommandHandler {
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;
}
@@ -69,7 +73,7 @@ export class LoginCommandHandler {
userState: user.state,
stateDescription
});
return null;
throw new Error(errorMessage);
}
try {
@@ -86,11 +90,11 @@ export class LoginCommandHandler {
userId: user.id,
username: cmd.username
});
return null;
throw new Error('Invalid password');
}
} catch (error) {
logError('Password verification error', error as Error);
return null;
throw new Error('Invalid password');
}
const mockRes = {
@@ -174,8 +178,12 @@ export class LoginCommandHandler {
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' ||
// 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;
}
@@ -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;
}
}
}
@@ -1,17 +1,17 @@
import { IUserRepository } from '../../../Domain/IRepository/IUserRepository';
import { GetUserByIdQuery } from './GetUserByIdQuery';
import { ShortUserDto } from '../../DTOs/UserDto';
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<ShortUserDto | null> {
async execute(query: GetUserByIdQuery): Promise<DetailUserDto | null> {
try {
const user = await this.userRepo.findById(query.id);
if (!user) return null;
return UserMapper.toShortDto(user);
return UserMapper.toDetailDto(user);
} catch (error) {
logError('GetUserByIdQueryHandler error', error instanceof Error ? error : new Error(String(error)));