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,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) {
|
||||
|
||||
Reference in New Issue
Block a user