Files
SerpentRace/SerpentRace_Backend/src/Application/User/commands/LoginCommandHandler.ts
T

189 lines
6.4 KiB
TypeScript

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');
}
}
}