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:
Aditya
2026-05-10 13:24:48 +05:30
parent 2d4fb4237e
commit 3b752dc6ae
56 changed files with 7741 additions and 599 deletions
@@ -69,9 +69,9 @@ export class EditClientComponent {
@ViewChild(ClientGeneralStepComponent) clientGeneralStep: ClientGeneralStepComponent;
@ViewChild('clientAddress') clientAddressStep: ClientAddressStepComponent;
@ViewChild('clientIndiaAddress') clientIndiaAddressStep: ClientIndiaAddressStepComponent;
@ViewChild('clientEntityKyc') clientEntityKycStep: ClientEntityKycStepComponent;
@ViewChild(ClientEntityKycStepComponent) clientEntityKycStep: ClientEntityKycStepComponent;
@ViewChild('clientKyc') clientKycStep: ClientKycStepComponent;
@ViewChild('clientFamilyMembers') clientFamilyMembersStep: ClientFamilyMembersStepComponent;
@ViewChild(ClientFamilyMembersStepComponent) clientFamilyMembersStep: ClientFamilyMembersStepComponent;
@ViewChildren('dtclient') clientDatatables: QueryList<ClientDatatableStepComponent>;
datatables: any = [];
@@ -184,13 +184,13 @@ export class EditClientComponent {
let dialogContext = 'Are you sure you want to save the changes';
let dialogType = 'basic';
// Check if DOB changed on the General tab — warn about re-KYC
// Check if name or DOB changed on the General tab — warn about re-KYC
if (this.selectedTabIndex === 0) {
const changes = this.detectSensitiveFieldChanges(this.clientDataAndTemplate);
if (changes.dobChanged) {
if (changes.nameChanged || changes.dobChanged) {
heading = 'Re-KYC Required';
dialogContext =
'Date of birth has been changed. This will require PAN re-verification in the KYC section. A remark will be added automatically. Do you want to proceed';
'Client name or date of birth has been changed. This will require PAN re-verification in the KYC section. A remark will be added automatically. Do you want to proceed';
dialogType = 'warn';
}
}
@@ -232,11 +232,11 @@ export class EditClientComponent {
// Log mobile/email changes as client notes for risk audit
this.logFieldChangeNotes(changedFields);
// If DOB changed, flag re-KYC requirement and reset PAN verification
if (changedFields.dobChanged) {
// If name or DOB changed, flag re-KYC requirement and reset PAN verification
if (changedFields.nameChanged || changedFields.dobChanged) {
this.dobChanged = true;
this.tabEditMode[1] = true; // auto-enable KYC tab for re-verification
this.flagReKycRequired(changedFields.oldDob, changedFields.newDob);
this.flagReKycRequired(changedFields);
}
// Update original data snapshot so subsequent edits compare against latest
@@ -249,6 +249,13 @@ export class EditClientComponent {
if (changedFields.dobChanged) {
originalData.dateOfBirth = changedFields.newDob;
}
if (changedFields.nameChanged) {
const form = this.clientGeneralForm;
originalData.firstname = form?.get('firstname')?.value ?? originalData.firstname;
originalData.middlename = form?.get('middlename')?.value ?? originalData.middlename;
originalData.lastname = form?.get('lastname')?.value ?? originalData.lastname;
originalData.fullname = form?.get('fullname')?.value ?? originalData.fullname;
}
this.selectedTabIndex = this.selectedTabIndex + 1;
},
@@ -261,6 +268,11 @@ export class EditClientComponent {
// KYC tab (always index 1)
if (tabIndex === 1) {
// Photograph + Signature are mandatory before leaving the KYC tab
if (this.clientKycStep && !this.clientKycStep.photoSigComplete) {
this.clientKycStep.markPhotoSigTouched();
return;
}
this.savingClient = true;
// Upload photo if selected
@@ -317,6 +329,8 @@ export class EditClientComponent {
if (!kycId && res?.resourceId) {
this.clientKycStep.kycId = res.resourceId.toString();
}
// Persist KYC document picker selection + uploaded files as a client identifier
this.saveKycDocProof(this.clientId);
this.selectedTabIndex = this.selectedTabIndex + 1;
},
error: () => {
@@ -363,7 +377,7 @@ export class EditClientComponent {
}
/**
* Compare current form values with original data to detect changes in DOB, mobile, email.
* Compare current form values with original data to detect changes in name, DOB, mobile, email.
*/
private detectSensitiveFieldChanges(originalData: any) {
const form = this.clientGeneralForm;
@@ -381,10 +395,38 @@ export class EditClientComponent {
? oldDobRaw.toISOString().split('T')[0]
: oldDobRaw || '';
const buildName = (parts: { firstname?: string; middlename?: string; lastname?: string; fullname?: string }) =>
(
parts.fullname ||
[
parts.firstname,
parts.middlename,
parts.lastname
]
.filter((p) => p && p.trim())
.join(' ')
).trim();
const newName = buildName({
firstname: form?.get('firstname')?.value || '',
middlename: form?.get('middlename')?.value || '',
lastname: form?.get('lastname')?.value || '',
fullname: form?.get('fullname')?.value || ''
});
const oldName = buildName({
firstname: originalData.firstname || '',
middlename: originalData.middlename || '',
lastname: originalData.lastname || '',
fullname: originalData.fullname || ''
});
return {
nameChanged: newName !== oldName,
mobileChanged: newMobile !== oldMobile,
emailChanged: newEmail !== oldEmail,
dobChanged: newDob !== oldDob,
oldName,
newName,
oldMobile,
newMobile,
oldEmail,
@@ -412,22 +454,38 @@ export class EditClientComponent {
}
/**
* When DOB changes, reset PAN verification in KYC step and pre-fill remark for re-KYC.
* When name or DOB changes, reset PAN verification in KYC step and pre-fill remark for re-KYC.
*/
private flagReKycRequired(oldDob: string, newDob: string) {
private flagReKycRequired(changes: {
nameChanged: boolean;
dobChanged: boolean;
oldName: string;
newName: string;
oldDob: string;
newDob: string;
}) {
const reasons: string[] = [];
if (changes.nameChanged) {
reasons.push(`Client name changed from "${changes.oldName}" to "${changes.newName}"`);
}
if (changes.dobChanged) {
reasons.push(`Date of birth changed from "${changes.oldDob}" to "${changes.newDob}"`);
}
const reasonText = reasons.join('; ');
// Reset PAN verification status so user must re-verify
if (this.clientKycStep) {
this.clientKycStep.panVerifyStatus = 'NONE';
this.clientKycStep.panVerifyName = '';
this.clientKycStep.panVerifiedOn = '';
// Pre-fill remarks with re-KYC note
const remarkText = `[Re-KYC Required] Date of birth changed from ${oldDob} to ${newDob}. PAN re-verification is mandatory.`;
const remarkText = `[Re-KYC Required] ${reasonText}. PAN re-verification is mandatory.`;
this.clientKycStep.kycForm.get('remarks')?.setValue(remarkText);
}
// Also log DOB change as an audit note
// Also log the change as an audit note
const timestamp = new Date().toISOString();
const note = `[AUDIT] Date of birth changed from "${oldDob}" to "${newDob}" on ${timestamp}. Re-KYC (PAN re-verification) required.`;
const note = `[AUDIT] ${reasonText} on ${timestamp}. Re-KYC (PAN re-verification) required.`;
this.clientsService.createClientNote(this.clientId, { note }).subscribe({ error: () => {} });
}
@@ -439,6 +497,13 @@ export class EditClientComponent {
* Submits the edit client form.
*/
submit() {
// Photograph + Signature are mandatory before submitting
if (this.clientKycStep && !this.clientKycStep.photoSigComplete) {
this.clientKycStep.markPhotoSigTouched();
this.selectedTabIndex = 1;
return;
}
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const clientData = {
@@ -496,10 +561,14 @@ export class EditClientComponent {
kycObs = this.clientsService.saveClientKyc(this.clientId, kycData);
}
kycObs.subscribe({
next: () => navigateBack(),
next: () => {
this.saveKycDocProof(this.clientId);
navigateBack();
},
error: () => navigateBack()
});
} else {
this.saveKycDocProof(this.clientId);
navigateBack();
}
},
@@ -580,6 +649,72 @@ export class EditClientComponent {
},
error: () => {}
});
// Persist the selected proof of address as a client identifier (with attached files)
this.saveAddressProof(clientId);
}
/**
* Save the address-proof picked on the permanent tab as a client identifier
* + upload each selected file as a document attached to that identifier.
* Mirrors identities-tab.createIdentifierWithDocs.
*/
private saveAddressProof(clientId: string) {
const payload = this.clientIndiaAddressStep?.addressProofPayload;
if (!payload?.proofId) return;
const proofName = payload.proof?.name || 'Address Proof';
const identifierData: any = {
documentTypeId: payload.proofId,
status: 'Active',
documentKey: payload.documentKey || `${proofName} - ${clientId}`,
description: payload.issueDate ? `Issue date: ${payload.issueDate}` : ''
};
this.clientsService.addClientIdentifier(clientId, identifierData).subscribe({
next: (res: any) => {
const identifierId = res?.resourceId;
if (!identifierId) return;
for (const entry of payload.files) {
const formData = new FormData();
formData.append('name', `${proofName} — ${entry.slot}`);
formData.append('file', entry.file);
this.clientsService.uploadClientIdentifierDocument(identifierId, formData).subscribe({ error: () => {} });
}
},
error: () => {}
});
}
/**
* Save the KYC document picked on the KYC tab as a client identifier
* + upload each selected file as a document attached to that identifier.
*/
private saveKycDocProof(clientId: string) {
const payload = this.clientKycStep?.kycDocPayload;
if (!payload?.proofId || !payload?.files?.length) return;
const proofName = payload.proof?.name || 'KYC Document';
const identifierData: any = {
documentTypeId: payload.proofId,
status: 'Active',
documentKey: payload.documentKey || `${proofName} - ${clientId}`,
description: 'KYC document'
};
this.clientsService.addClientIdentifier(clientId, identifierData).subscribe({
next: (res: any) => {
const identifierId = res?.resourceId;
if (!identifierId) return;
for (const entry of payload.files) {
const formData = new FormData();
formData.append('name', `${proofName} — ${entry.slot}`);
formData.append('file', entry.file);
this.clientsService.uploadClientIdentifierDocument(identifierId, formData).subscribe({ error: () => {} });
}
},
error: () => {}
});
}
private saveClientFamilyMembers(clientId: string) {