feat(auth): prevent soft-deleted users from authenticating (GOO-15)
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>
This commit is contained in:
Ho Ngoc Hai
2026-04-22 23:40:43 +07:00
parent 23af73496d
commit 8706fff92f
4 changed files with 69 additions and 0 deletions

View File

@@ -17,6 +17,7 @@ export interface UserProps {
kycStatus: KYCStatus;
kycData: unknown;
isActive: boolean;
deletedAt: Date | null;
totpSecret: string | null;
totpEnabled: boolean;
totpBackupCodes: string[];
@@ -33,6 +34,7 @@ export class UserEntity extends AggregateRoot<string> {
private _kycStatus: KYCStatus;
private _kycData: unknown;
private _isActive: boolean;
private _deletedAt: Date | null;
private _totpSecret: string | null;
private _totpEnabled: boolean;
private _totpBackupCodes: string[];
@@ -49,6 +51,7 @@ export class UserEntity extends AggregateRoot<string> {
this._kycStatus = props.kycStatus;
this._kycData = props.kycData;
this._isActive = props.isActive;
this._deletedAt = props.deletedAt;
this._totpSecret = props.totpSecret;
this._totpEnabled = props.totpEnabled;
this._totpBackupCodes = props.totpBackupCodes;
@@ -64,6 +67,7 @@ export class UserEntity extends AggregateRoot<string> {
get kycStatus(): KYCStatus { return this._kycStatus; }
get kycData(): unknown { return this._kycData; }
get isActive(): boolean { return this._isActive; }
get deletedAt(): Date | null { return this._deletedAt; }
get totpSecret(): string | null { return this._totpSecret; }
get totpEnabled(): boolean { return this._totpEnabled; }
get totpBackupCodes(): string[] { return this._totpBackupCodes; }
@@ -87,6 +91,44 @@ export class UserEntity extends AggregateRoot<string> {
kycStatus: 'NONE',
kycData: null,
isActive: true,
deletedAt: null,
totpSecret: null,
totpEnabled: false,
totpBackupCodes: [],
totpEnabledAt: null,
});
user.addDomainEvent(new UserRegisteredEvent(id, phone.value, role));
return user;
}
/**
* Create a passwordless user (e.g. via Phone-OTP login auto-register).
* `passwordHash` is null so password login is not possible until the user
* sets one via the password-reset / profile flow. A fullName fallback is
* used since OTP signup does not collect a name.
*/
static createPasswordless(
id: string,
phone: Phone,
fullName?: string,
role: UserRole = 'BUYER',
): UserEntity {
const displayName =
fullName && fullName.trim().length > 0
? fullName
: `Người dùng ${phone.value.slice(-4)}`;
const user = new UserEntity(id, {
email: null,
phone,
passwordHash: null,
fullName: displayName,
avatarUrl: null,
role,
kycStatus: 'NONE',
kycData: null,
isActive: true,
deletedAt: null,
totpSecret: null,
totpEnabled: false,
totpBackupCodes: [],