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
@@ -72,9 +72,9 @@ export class CreateClientComponent {
@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 = [];
@@ -250,6 +250,13 @@ export class CreateClientComponent {
// KYC tab
const kycTabIndex = 1;
if (tabIndex === kycTabIndex) {
// Photograph + Signature are mandatory before leaving the KYC tab
if (this.clientKycStep && !this.clientKycStep.photoSigComplete) {
this.clientKycStep.markPhotoSigTouched();
this.validationError = 'Photograph and Signature are required before continuing.';
return;
}
this.validationError = '';
// Save KYC
if (this.clientKycStep?.hasKycData && !this.clientKycStep.kycId) {
this.clientsService.saveClientKyc(clientId, this.clientKycStep.kycData).subscribe({ error: () => {} });
@@ -265,6 +272,8 @@ export class CreateClientComponent {
.uploadClientSignatureImage(clientId, this.clientKycStep.signatureFile)
.subscribe({ error: () => {} });
}
// Persist the KYC document picker selection + uploaded files as a client identifier
this.saveKycDocProof(clientId);
return;
}
@@ -349,6 +358,14 @@ export class CreateClientComponent {
validationError = '';
submit() {
// Validate: photograph and signature are mandatory
if (this.clientKycStep && !this.clientKycStep.photoSigComplete) {
this.clientKycStep.markPhotoSigTouched();
this.validationError = 'Photograph and Signature are required before submitting.';
// Switch to KYC tab
this.selectedTabIndex = 1;
return;
}
// Validate: at least one family member required (Individual clients)
if (this.legalFormType === 1 && this.clientFamilyMembersStep && !this.clientFamilyMembersStep.hasFamilyMembers) {
this.validationError = 'At least one family member is required. Please add a family member before submitting.';
@@ -425,6 +442,9 @@ export class CreateClientComponent {
this.saveClientFamilyMembers(clientId);
}
// Persist the KYC document picker selection + uploaded files as a client identifier
this.saveKycDocProof(clientId);
// Save KYC only if it wasn't already created during the KYC step
if (this.clientKycStep?.hasKycData && !this.clientKycStep.kycId) {
this.clientsService.saveClientKyc(clientId, this.clientKycStep.kycData).subscribe({
@@ -514,6 +534,72 @@ export class CreateClientComponent {
},
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: () => {}
});
}
/**