189 lines
6.4 KiB
TypeScript
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');
|
|
}
|
|
}
|
|
}
|