Files
goodgo-platform/apps/api/src/modules/auth/infrastructure/strategies/local.strategy.ts
Ho Ngoc Hai 8706fff92f
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
feat(auth): prevent soft-deleted users from authenticating (GOO-15)
- 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>
2026-04-22 23:40:43 +07:00

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