LocalStrategy.validate lacked a try-catch, so infrastructure errors (DB timeouts, bcrypt failures, null/undefined phone) escaped as raw Error instances. LocalAuthGuard.handleRequest blindly re-threw them, causing GlobalExceptionFilter to map them to 500 Internal Server Error instead of 401 Unauthorized. Changes: - Add null/falsy guard for phone and password in LocalStrategy.validate - Wrap validate body in try-catch; re-throw DomainExceptions, wrap unexpected errors as UnauthorizedException (401) - Add error type-checking in LocalAuthGuard.handleRequest: re-throw HttpException subclasses directly, wrap other errors as 401 - Add @IsNotEmpty() validators to LoginDto for Swagger accuracy - Add 5 new test cases covering undefined/null/empty inputs, DB errors, and bcrypt failures - Update guard tests for the new type-checking behaviour Resolves TEC-1841 Co-Authored-By: Paperclip <noreply@paperclip.ing>
54 lines
2.0 KiB
TypeScript
54 lines
2.0 KiB
TypeScript
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
import { PassportStrategy } from '@nestjs/passport';
|
|
import { Strategy } from 'passport-local';
|
|
import { DomainException, normalizeVietnamPhone, UnauthorizedException } from '@modules/shared';
|
|
import { USER_REPOSITORY, type IUserRepository } from '../../domain/repositories/user.repository';
|
|
|
|
@Injectable()
|
|
export class LocalStrategy extends PassportStrategy(Strategy) {
|
|
private readonly logger = new Logger(LocalStrategy.name);
|
|
|
|
constructor(
|
|
@Inject(USER_REPOSITORY)
|
|
private readonly userRepo: IUserRepository,
|
|
) {
|
|
super({ usernameField: 'phone', passwordField: 'password' });
|
|
}
|
|
|
|
async validate(phone: string, password: string): Promise<{ id: string; phone: string; role: string }> {
|
|
try {
|
|
if (!phone || !password) {
|
|
throw new UnauthorizedException('Số điện thoại hoặc mật khẩu không đúng');
|
|
}
|
|
|
|
const normalizedPhone = normalizeVietnamPhone(phone);
|
|
if (!normalizedPhone) {
|
|
throw new UnauthorizedException('Số điện thoại không hợp lệ');
|
|
}
|
|
const user = await this.userRepo.findByPhone(normalizedPhone);
|
|
|
|
if (!user || !user.passwordHash) {
|
|
throw new UnauthorizedException('Số điện thoại hoặc mật khẩu không đúng');
|
|
}
|
|
|
|
if (!user.isActive) {
|
|
throw new UnauthorizedException('Tài khoản đã bị vô hiệu hóa');
|
|
}
|
|
|
|
const isValid = await user.passwordHash.compare(password);
|
|
if (!isValid) {
|
|
throw new UnauthorizedException('Số điện thoại hoặc mật khẩu không đúng');
|
|
}
|
|
|
|
return { id: user.id, phone: user.phone.value, role: user.role };
|
|
} catch (error) {
|
|
if (error instanceof DomainException) throw error;
|
|
this.logger.error(
|
|
`Authentication failed: ${error instanceof Error ? error.message : error}`,
|
|
error instanceof Error ? error.stack : undefined,
|
|
);
|
|
throw new UnauthorizedException('Số điện thoại hoặc mật khẩu không đúng');
|
|
}
|
|
}
|
|
}
|