Some checks failed
CI / AI Services (Python) — Smoke (push) Failing after 26s
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 2m37s
Deploy / Build Web Image (push) Failing after 1m9s
Deploy / Build AI Services Image (push) Failing after 37s
E2E Tests / Playwright E2E (push) Failing after 56s
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 11m58s
Deploy / Build API Image (push) Failing after 12m43s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 9s
Security Scanning / Trivy Scan — API Image (push) Failing after 1m27s
Security Scanning / Trivy Scan — Web Image (push) Failing after 43s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 30s
Security Scanning / Trivy Filesystem Scan (push) Failing after 32s
Security Scanning / Security Gate (push) Failing after 1s
CI / E2E Tests (push) Has been cancelled
Deploy / Deploy to Staging (push) Has been cancelled
Deploy / Smoke Test Staging (push) Has been cancelled
Deploy / Rollback Staging (push) Has been cancelled
Deploy / Deploy to Production (push) Has been cancelled
Deploy / Smoke Test Production (push) Has been cancelled
Deploy / Rollback Production (push) Has been cancelled
- Add deletedAt field to UserProps interface and UserEntity - Map raw.deletedAt in PrismaUserRepository.toDomain() - Check deletedAt !== null in LocalStrategy.validate() → 401 Tài khoản đã bị xóa - Update existing LocalStrategy tests with deletedAt: null on valid mocks - Add test: soft-deleted user login → 401 Co-Authored-By: Paperclip <noreply@paperclip.ing>
70 lines
2.3 KiB
TypeScript
70 lines
2.3 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';
|
|
|
|
export interface LocalStrategyResult {
|
|
id: string;
|
|
phone: string;
|
|
role: string;
|
|
isMfaRequired: boolean;
|
|
}
|
|
|
|
@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<LocalStrategyResult> {
|
|
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');
|
|
}
|
|
|
|
if (user.deletedAt !== null) {
|
|
throw new UnauthorizedException('Tài khoản đã bị xó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,
|
|
isMfaRequired: user.totpEnabled,
|
|
};
|
|
} 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');
|
|
}
|
|
}
|
|
}
|