feat: MFA/2FA, OTP audit logs, KYC + client/user enhancements
- Access Control module: SMS/Email/TOTP/Hardware-token MFA factor management - OTP Audit Logs module under Admin menu (list + detail) with 401/403 handling - Two-factor authentication login flow updates - Client KYC, India address, family-members, ckyc, nominee enhancements - New shared KYC helpers (kyc-proof-types, kyc-upload-helper.service) - User create/edit/view enhancements - Shell navigation: OTP Audit Logs entry under real Admin menu
This commit is contained in:
+69
-18
@@ -27,7 +27,7 @@ import {
|
||||
UntypedFormControl,
|
||||
ReactiveFormsModule
|
||||
} from '@angular/forms';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Subject, Subscription, merge } from 'rxjs';
|
||||
import { filter, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { ClientsService } from 'app/clients/clients.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
@@ -340,6 +340,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
sanctionsBlocked = false;
|
||||
/** Screening log ID from sanctions API */
|
||||
screeningLogId: number | null = null;
|
||||
/** Snapshot of the name that was last verified — used to invalidate verification when name changes */
|
||||
private verifiedName: string = '';
|
||||
|
||||
/**
|
||||
* @param {FormBuilder} formBuilder Form Builder
|
||||
@@ -500,6 +502,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
}
|
||||
if (data.nameVerified === true || data.nameVerified === 'true' || data.nameVerified === 1) {
|
||||
this.sanctionsVerified = true;
|
||||
this.verifiedName = this.currentNameSnapshot();
|
||||
} else if (data.nameVerified === false || data.nameVerified === 'false' || data.nameVerified === 0) {
|
||||
this.sanctionsVerified = false;
|
||||
}
|
||||
@@ -701,6 +704,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
})
|
||||
);
|
||||
}
|
||||
// Re-attach name-change watchers after controls are (re)created
|
||||
setTimeout(() => this.watchNameChangesForReverification());
|
||||
});
|
||||
// Initialize legal form — use client's existing value in edit mode, default to PERSON for create
|
||||
const initialLegalForm = this.clientData?.legalForm?.id || LegalFormId.PERSON;
|
||||
@@ -901,20 +906,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
* If a match is found, sanctionsBlocked is set to true blocking further steps.
|
||||
*/
|
||||
verifySanctions() {
|
||||
const firstname = this.createClientForm.get('firstname')?.value || '';
|
||||
const middlename = this.createClientForm.get('middlename')?.value || '';
|
||||
const lastname = this.createClientForm.get('lastname')?.value || '';
|
||||
const fullname = this.createClientForm.get('fullname')?.value || '';
|
||||
|
||||
const name =
|
||||
fullname ||
|
||||
[
|
||||
firstname,
|
||||
middlename,
|
||||
lastname
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const name = this.currentNameSnapshot();
|
||||
|
||||
const dialogRef = this.dialog.open(SanctionsVerifyDialogComponent, {
|
||||
width: '550px',
|
||||
@@ -927,14 +919,62 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
// CLEAR — no match found
|
||||
this.sanctionsVerified = true;
|
||||
this.sanctionsBlocked = false;
|
||||
this.verifiedName = name;
|
||||
} else if (result.verified === false) {
|
||||
// FLAGGED — match found, block account opening
|
||||
this.sanctionsVerified = false;
|
||||
this.sanctionsBlocked = true;
|
||||
this.verifiedName = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Concatenated current name from the form (handles individual + entity) */
|
||||
private currentNameSnapshot(): string {
|
||||
const firstname = this.createClientForm.get('firstname')?.value || '';
|
||||
const middlename = this.createClientForm.get('middlename')?.value || '';
|
||||
const lastname = this.createClientForm.get('lastname')?.value || '';
|
||||
const fullname = this.createClientForm.get('fullname')?.value || '';
|
||||
return (
|
||||
fullname ||
|
||||
[
|
||||
firstname,
|
||||
middlename,
|
||||
lastname
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch name fields — once a name has been verified, any edit invalidates the
|
||||
* sanctions verification badge so the user must re-verify before submitting.
|
||||
*/
|
||||
private nameWatchSub: Subscription | null = null;
|
||||
private watchNameChangesForReverification() {
|
||||
this.nameWatchSub?.unsubscribe();
|
||||
const fields = [
|
||||
'firstname',
|
||||
'middlename',
|
||||
'lastname',
|
||||
'fullname'
|
||||
];
|
||||
const controls = fields.map((f) => this.createClientForm.get(f)).filter((c) => !!c);
|
||||
if (!controls.length) return;
|
||||
this.nameWatchSub = merge(...controls.map((c) => c!.valueChanges))
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
// Only invalidate if a verification was previously recorded
|
||||
if (!this.sanctionsVerified && !this.sanctionsBlocked) return;
|
||||
if (this.currentNameSnapshot() !== this.verifiedName) {
|
||||
this.sanctionsVerified = false;
|
||||
this.sanctionsBlocked = false;
|
||||
this.screeningLogId = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Search existing clients to select as guardian */
|
||||
onGuardianSearch(text: string): void {
|
||||
this.guardianSearchText = text;
|
||||
@@ -947,7 +987,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
|
||||
this.guardianSearching = true;
|
||||
this.guardianSearchTimer = setTimeout(() => {
|
||||
this.clientService.searchByText(text, 0, 10).subscribe({
|
||||
this.clientService.searchByText(text, 0, 10, '', '', true).subscribe({
|
||||
next: (data: any) => {
|
||||
this.guardianSearchResults = data?.content || [];
|
||||
this.guardianSearching = false;
|
||||
@@ -1106,7 +1146,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
}
|
||||
this.signatorySearching = true;
|
||||
this.signatorySearchTimer = setTimeout(() => {
|
||||
this.clientService.searchByText(text, 0, 10).subscribe({
|
||||
this.clientService.searchByText(text, 0, 10, '', '', true).subscribe({
|
||||
next: (data: any) => {
|
||||
this.signatorySearchResults = data?.content || [];
|
||||
this.signatorySearching = false;
|
||||
@@ -1623,8 +1663,19 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
const generalDetails = this.readOnly ? this.createClientForm.getRawValue() : this.createClientForm.value;
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const locale = this.settingsService.language.code;
|
||||
const isEditMode = !!this.clientData;
|
||||
for (const key in generalDetails) {
|
||||
if (generalDetails[key] === '' || key === 'addSavings') {
|
||||
if (key === 'addSavings') {
|
||||
delete generalDetails[key];
|
||||
continue;
|
||||
}
|
||||
if (generalDetails[key] === '') {
|
||||
// In edit mode, preserve empty strings for fields that previously had a value so the
|
||||
// backend can clear them (Fineract treats a missing field as "no change" but an empty
|
||||
// string as a deliberate clear).
|
||||
if (isEditMode && this.clientData[key] != null && this.clientData[key] !== '') {
|
||||
continue;
|
||||
}
|
||||
delete generalDetails[key];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user