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:
@@ -17,6 +17,10 @@ export interface UserProps {
|
||||
kycStatus: KYCStatus;
|
||||
kycData: unknown;
|
||||
isActive: boolean;
|
||||
totpSecret: string | null;
|
||||
totpEnabled: boolean;
|
||||
totpBackupCodes: string[];
|
||||
totpEnabledAt: Date | null;
|
||||
}
|
||||
|
||||
export class UserEntity extends AggregateRoot<string> {
|
||||
@@ -29,6 +33,10 @@ export class UserEntity extends AggregateRoot<string> {
|
||||
private _kycStatus: KYCStatus;
|
||||
private _kycData: unknown;
|
||||
private _isActive: boolean;
|
||||
private _totpSecret: string | null;
|
||||
private _totpEnabled: boolean;
|
||||
private _totpBackupCodes: string[];
|
||||
private _totpEnabledAt: Date | null;
|
||||
|
||||
constructor(id: string, props: UserProps, createdAt?: Date, updatedAt?: Date) {
|
||||
super(id, createdAt, updatedAt);
|
||||
@@ -41,6 +49,10 @@ export class UserEntity extends AggregateRoot<string> {
|
||||
this._kycStatus = props.kycStatus;
|
||||
this._kycData = props.kycData;
|
||||
this._isActive = props.isActive;
|
||||
this._totpSecret = props.totpSecret;
|
||||
this._totpEnabled = props.totpEnabled;
|
||||
this._totpBackupCodes = props.totpBackupCodes;
|
||||
this._totpEnabledAt = props.totpEnabledAt;
|
||||
}
|
||||
|
||||
get email(): Email | null { return this._email; }
|
||||
@@ -52,6 +64,10 @@ 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 totpSecret(): string | null { return this._totpSecret; }
|
||||
get totpEnabled(): boolean { return this._totpEnabled; }
|
||||
get totpBackupCodes(): string[] { return this._totpBackupCodes; }
|
||||
get totpEnabledAt(): Date | null { return this._totpEnabledAt; }
|
||||
|
||||
static createNew(
|
||||
id: string,
|
||||
@@ -71,6 +87,10 @@ export class UserEntity extends AggregateRoot<string> {
|
||||
kycStatus: 'NONE',
|
||||
kycData: null,
|
||||
isActive: true,
|
||||
totpSecret: null,
|
||||
totpEnabled: false,
|
||||
totpBackupCodes: [],
|
||||
totpEnabledAt: null,
|
||||
});
|
||||
|
||||
user.addDomainEvent(new UserRegisteredEvent(id, phone.value, role));
|
||||
@@ -97,4 +117,25 @@ export class UserEntity extends AggregateRoot<string> {
|
||||
this._isActive = true;
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
enableTotp(secret: string, backupCodes: string[]): void {
|
||||
this._totpSecret = secret;
|
||||
this._totpEnabled = true;
|
||||
this._totpBackupCodes = backupCodes;
|
||||
this._totpEnabledAt = new Date();
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
disableTotp(): void {
|
||||
this._totpSecret = null;
|
||||
this._totpEnabled = false;
|
||||
this._totpBackupCodes = [];
|
||||
this._totpEnabledAt = null;
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
consumeBackupCode(index: number): void {
|
||||
this._totpBackupCodes = this._totpBackupCodes.filter((_, i) => i !== index);
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,3 +4,8 @@ export {
|
||||
type IRefreshTokenRepository,
|
||||
type RefreshTokenRecord,
|
||||
} from './refresh-token.repository';
|
||||
export {
|
||||
MFA_CHALLENGE_REPOSITORY,
|
||||
type IMfaChallengeRepository,
|
||||
type MfaChallengeRecord,
|
||||
} from './mfa-challenge.repository';
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export const MFA_CHALLENGE_REPOSITORY = Symbol('MFA_CHALLENGE_REPOSITORY');
|
||||
|
||||
export interface MfaChallengeRecord {
|
||||
id: string;
|
||||
userId: string;
|
||||
type: string;
|
||||
attemptCount: number;
|
||||
maxAttempts: number;
|
||||
isVerified: boolean;
|
||||
expiresAt: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface IMfaChallengeRepository {
|
||||
create(record: Omit<MfaChallengeRecord, 'createdAt'>): Promise<MfaChallengeRecord>;
|
||||
findById(id: string): Promise<MfaChallengeRecord | null>;
|
||||
incrementAttempts(id: string): Promise<void>;
|
||||
markVerified(id: string): Promise<void>;
|
||||
deleteExpired(): Promise<number>;
|
||||
deleteByUserId(userId: string): Promise<number>;
|
||||
}
|
||||
@@ -8,4 +8,8 @@ export interface IUserRepository {
|
||||
findByEmail(email: string): Promise<UserEntity | null>;
|
||||
save(user: UserEntity): Promise<void>;
|
||||
update(user: UserEntity): Promise<void>;
|
||||
updateMfaSecret(userId: string, secret: string | null): Promise<void>;
|
||||
updateMfaEnabled(userId: string, enabled: boolean, secret: string, backupCodes: string[]): Promise<void>;
|
||||
updateMfaDisabled(userId: string): Promise<void>;
|
||||
updateBackupCodes(userId: string, backupCodes: string[]): Promise<void>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user