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
@@ -27,7 +27,8 @@
<div class="cu-grid" #userFormRef>
<mat-form-field appearance="outline" class="cu-field">
<mat-label>{{ 'labels.inputs.Username' | translate }}</mat-label>
<input matInput required formControlName="username" />
<span matPrefix class="cu-prefix-icon"><mat-icon>person</mat-icon></span>
<input matInput required formControlName="username" placeholder="Login username" />
@if (userForm.controls.username.hasError('required')) {
<mat-error
>{{ 'labels.inputs.Username' | translate }} {{ 'labels.commons.is' | translate }}
@@ -36,26 +37,10 @@
}
</mat-form-field>
<mat-form-field appearance="outline" class="cu-field">
<mat-label>{{ 'labels.inputs.Email' | translate }}</mat-label>
<input matInput [required]="userForm.controls.sendPasswordToEmail.value" formControlName="email" />
@if (userForm.controls.email.hasError('email')) {
<mat-error
>{{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.invalid' | translate }}</strong></mat-error
>
}
@if (userForm.controls.email.hasError('required')) {
<mat-error
>{{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong></mat-error
>
}
</mat-form-field>
<mat-form-field appearance="outline" class="cu-field">
<mat-label>{{ 'labels.inputs.First Name' | translate }}</mat-label>
<input matInput required formControlName="firstname" />
<span matPrefix class="cu-prefix-icon"><mat-icon>badge</mat-icon></span>
<input matInput required formControlName="firstname" placeholder="Given name" />
@if (userForm.controls.firstname.hasError('pattern')) {
<mat-error
>{{ 'labels.inputs.First Name' | translate }}
@@ -74,7 +59,8 @@
<mat-form-field appearance="outline" class="cu-field">
<mat-label>{{ 'labels.inputs.Last Name' | translate }}</mat-label>
<input matInput required formControlName="lastname" />
<span matPrefix class="cu-prefix-icon"><mat-icon>person_outline</mat-icon></span>
<input matInput required formControlName="lastname" placeholder="Family name" />
@if (userForm.controls.lastname.hasError('pattern')) {
<mat-error
>{{ 'labels.inputs.Last Name' | translate }}
@@ -90,6 +76,54 @@
>
}
</mat-form-field>
<mat-form-field appearance="outline" class="cu-field">
<mat-label>{{ 'labels.inputs.Email' | translate }}</mat-label>
<span matPrefix class="cu-prefix-icon"><mat-icon>email</mat-icon></span>
<input
matInput
[required]="userForm.controls.sendPasswordToEmail.value"
formControlName="email"
placeholder="name@example.com"
/>
@if (userForm.controls.email.hasError('email')) {
<mat-error
>{{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.invalid' | translate }}</strong></mat-error
>
}
@if (userForm.controls.email.hasError('required')) {
<mat-error
>{{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong></mat-error
>
}
</mat-form-field>
<mat-form-field appearance="outline" class="cu-field">
<mat-label>Mobile Number</mat-label>
<span matPrefix class="cu-mobile-prefix">
<mat-icon>phone_android</mat-icon>
<span class="cu-mobile-prefix__code">+91</span>
</span>
<input
matInput
required
formControlName="mobileNo"
maxlength="10"
placeholder="10-digit mobile number"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
/>
@if (userForm.controls.mobileNo.hasError('required')) {
<mat-error
>Mobile number {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
@if (userForm.controls.mobileNo.hasError('pattern')) {
<mat-error>Enter a valid <strong>10-digit mobile number</strong></mat-error>
}
</mat-form-field>
</div>
</div>
@@ -179,6 +213,7 @@
<div class="cu-grid">
<mat-form-field appearance="outline" class="cu-field">
<mat-label>{{ 'labels.inputs.Office' | translate }}</mat-label>
<span matPrefix class="cu-prefix-icon"><mat-icon>business</mat-icon></span>
<mat-select required formControlName="officeId">
@for (office of officesData; track office) {
<mat-option [value]="office.id">{{ office.name }}</mat-option>
@@ -194,6 +229,7 @@
<mat-form-field appearance="outline" class="cu-field">
<mat-label>{{ 'labels.inputs.Staff' | translate }}</mat-label>
<span matPrefix class="cu-prefix-icon"><mat-icon>work</mat-icon></span>
<mat-select formControlName="staffId">
@for (staff of staffData; track staff) {
<mat-option [value]="staff.id">{{ staff.displayName }}</mat-option>
@@ -203,6 +239,7 @@
<mat-form-field appearance="outline" class="cu-field cu-field--full">
<mat-label>{{ 'labels.inputs.Roles' | translate }}</mat-label>
<span matPrefix class="cu-prefix-icon"><mat-icon>verified_user</mat-icon></span>
<mat-select required formControlName="roles" multiple>
@for (role of rolesData; track role) {
<mat-option [value]="role.id">{{ role.name }}</mat-option>
@@ -110,6 +110,50 @@ $gray-900: #202124;
}
}
// Prefix icon (matches client-creation field-prefix-icon)
.cu-prefix-icon {
display: inline-flex;
align-items: center;
padding-left: 4px;
padding-right: 8px;
margin-right: 8px;
border-right: 1.5px solid #cbd5e1;
line-height: 1;
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
color: $primary;
}
}
// Combined icon + country-code prefix (mobile field)
.cu-mobile-prefix {
display: inline-flex;
align-items: center;
gap: 8px;
padding-left: 4px;
padding-right: 10px;
margin-right: 10px;
border-right: 1.5px solid #cbd5e1;
line-height: 1;
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
color: $primary;
}
&__code {
font-weight: 700;
font-size: 13px;
color: $gray-900;
letter-spacing: 0.3px;
}
}
.cu-checkbox-row {
grid-column: 1 / -1;
display: flex;
@@ -28,6 +28,7 @@ import { confirmPasswordValidator } from 'app/login/reset-password/confirm-passw
import { ConfigurationWizardService } from 'app/configuration-wizard/configuration-wizard.service';
import { ContinueSetupDialogComponent } from 'app/configuration-wizard/continue-setup-dialog/continue-setup-dialog.component';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatIcon } from '@angular/material/icon';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -39,7 +40,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
styleUrls: ['./create-user.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox
MatCheckbox,
MatIcon
]
})
export class CreateUserComponent implements OnInit, AfterViewInit {
@@ -108,6 +110,13 @@ export class CreateUserComponent implements OnInit, AfterViewInit {
Validators.email
]
],
mobileNo: [
'',
[
Validators.required,
Validators.pattern(/^\d{10}$/)
]
],
firstname: [
'',
[