fel kesz game backend
This commit is contained in:
@@ -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)));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user