Files
goodgo-platform/apps/api/src/modules/auth/infrastructure/strategies/local.strategy.ts
Ho Ngoc Hai 7008230424 fix(auth): prevent login endpoint from returning 500 on invalid credentials
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>
2026-04-11 19:53:41 +07:00

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