feat: add MFA/TOTP auth, PII encryption, agents/leads/inquiries modules, and comprehensive tests
- Add TOTP-based MFA with setup, verify, disable, backup codes, and challenge flow - Add PII field encryption middleware with AES-256-GCM and deterministic search hashes - Add agents, inquiries, and leads domain modules with entities, events, value objects - Add web dashboard pages for inquiries and leads with detail dialogs - Add 30+ component tests (valuation, charts, listings, search, providers, UI) - Add Prisma migrations for encryption hash columns and MFA TOTP support - Fix all ESLint errors (unused imports, duplicate imports, lint auto-fixes) - Update dependencies and lock file - Clean up obsolete exploration/QA docs, add audit documentation Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
export class VerifyMfaChallengeCommand {
|
||||
constructor(
|
||||
public readonly challengeId: string,
|
||||
public readonly totpCode: string,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { DomainException, type LoggerService, UnauthorizedException } from '@modules/shared';
|
||||
import {
|
||||
MFA_CHALLENGE_REPOSITORY,
|
||||
type IMfaChallengeRepository,
|
||||
} from '../../../domain/repositories/mfa-challenge.repository';
|
||||
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
|
||||
import { type MfaService } from '../../../infrastructure/services/mfa.service';
|
||||
import { type TokenService, type TokenPair } from '../../../infrastructure/services/token.service';
|
||||
import { VerifyMfaChallengeCommand } from './verify-mfa-challenge.command';
|
||||
|
||||
@CommandHandler(VerifyMfaChallengeCommand)
|
||||
export class VerifyMfaChallengeHandler implements ICommandHandler<VerifyMfaChallengeCommand> {
|
||||
constructor(
|
||||
@Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
|
||||
@Inject(MFA_CHALLENGE_REPOSITORY) private readonly challengeRepo: IMfaChallengeRepository,
|
||||
private readonly mfaService: MfaService,
|
||||
private readonly tokenService: TokenService,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(command: VerifyMfaChallengeCommand): Promise<TokenPair> {
|
||||
try {
|
||||
// Find and validate the challenge
|
||||
const challenge = await this.challengeRepo.findById(command.challengeId);
|
||||
if (!challenge) {
|
||||
throw new UnauthorizedException('Phiên xác thực MFA không tồn tại hoặc đã hết hạn');
|
||||
}
|
||||
|
||||
if (challenge.isVerified) {
|
||||
throw new UnauthorizedException('Phiên xác thực MFA đã được sử dụng');
|
||||
}
|
||||
|
||||
if (challenge.expiresAt < new Date()) {
|
||||
throw new UnauthorizedException('Phiên xác thực MFA đã hết hạn');
|
||||
}
|
||||
|
||||
if (challenge.attemptCount >= challenge.maxAttempts) {
|
||||
throw new UnauthorizedException('Đã vượt quá số lần thử. Vui lòng đăng nhập lại');
|
||||
}
|
||||
|
||||
// Look up the user
|
||||
const user = await this.userRepo.findById(challenge.userId);
|
||||
if (!user || !user.totpSecret || !user.totpEnabled) {
|
||||
throw new UnauthorizedException('MFA chưa được thiết lập cho tài khoản này');
|
||||
}
|
||||
|
||||
// Verify the TOTP code
|
||||
const isValid = await this.mfaService.verifyTotp(command.totpCode, user.totpSecret);
|
||||
if (!isValid) {
|
||||
await this.challengeRepo.incrementAttempts(command.challengeId);
|
||||
const remaining = challenge.maxAttempts - challenge.attemptCount - 1;
|
||||
throw new UnauthorizedException(
|
||||
`Mã TOTP không hợp lệ. Còn ${remaining} lần thử`,
|
||||
);
|
||||
}
|
||||
|
||||
// Mark the challenge as verified
|
||||
await this.challengeRepo.markVerified(command.challengeId);
|
||||
|
||||
// Generate token pair (login complete)
|
||||
return this.tokenService.generateTokenPair({
|
||||
sub: user.id,
|
||||
phone: user.phone.value,
|
||||
role: user.role,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to verify MFA challenge: ${error instanceof Error ? error.message : error}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name,
|
||||
);
|
||||
throw new InternalServerErrorException('Không thể xác thực MFA');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user