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: [
'',
[
@@ -27,7 +27,8 @@
<div class="eu-grid">
<mat-form-field appearance="outline" class="eu-field">
<mat-label>{{ 'labels.inputs.Username' | translate }}</mat-label>
<input matInput required formControlName="username" />
<span matPrefix class="eu-prefix-icon"><mat-icon>person</mat-icon></span>
<input matInput required formControlName="username" placeholder="Login username" />
@if (editUserForm.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="eu-field">
<mat-label>{{ 'labels.inputs.Email' | translate }}</mat-label>
<input matInput required formControlName="email" />
@if (editUserForm.controls.email.hasError('email')) {
<mat-error
>{{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.invalid' | translate }}</strong></mat-error
>
}
@if (editUserForm.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="eu-field">
<mat-label>{{ 'labels.inputs.First Name' | translate }}</mat-label>
<input matInput required formControlName="firstname" />
<span matPrefix class="eu-prefix-icon"><mat-icon>badge</mat-icon></span>
<input matInput required formControlName="firstname" placeholder="Given name" />
@if (editUserForm.controls.firstname.hasError('pattern')) {
<mat-error
>{{ 'labels.inputs.First Name' | translate }}
@@ -74,7 +59,8 @@
<mat-form-field appearance="outline" class="eu-field">
<mat-label>{{ 'labels.inputs.Last Name' | translate }}</mat-label>
<input matInput required formControlName="lastname" />
<span matPrefix class="eu-prefix-icon"><mat-icon>person_outline</mat-icon></span>
<input matInput required formControlName="lastname" placeholder="Family name" />
@if (editUserForm.controls.lastname.hasError('pattern')) {
<mat-error
>{{ 'labels.inputs.Last Name' | translate }}
@@ -90,6 +76,49 @@
>
}
</mat-form-field>
<mat-form-field appearance="outline" class="eu-field">
<mat-label>{{ 'labels.inputs.Email' | translate }}</mat-label>
<span matPrefix class="eu-prefix-icon"><mat-icon>email</mat-icon></span>
<input matInput required formControlName="email" placeholder="name@example.com" />
@if (editUserForm.controls.email.hasError('email')) {
<mat-error
>{{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.invalid' | translate }}</strong></mat-error
>
}
@if (editUserForm.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="eu-field">
<mat-label>Mobile Number</mat-label>
<span matPrefix class="eu-mobile-prefix">
<mat-icon>phone_android</mat-icon>
<span class="eu-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 (editUserForm.controls.mobileNo.hasError('required')) {
<mat-error
>Mobile number {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
@if (editUserForm.controls.mobileNo.hasError('pattern')) {
<mat-error>Enter a valid <strong>10-digit mobile number</strong></mat-error>
}
</mat-form-field>
</div>
</div>
@@ -117,6 +146,7 @@
<div class="eu-grid">
<mat-form-field appearance="outline" class="eu-field">
<mat-label>{{ 'labels.inputs.Office' | translate }}</mat-label>
<span matPrefix class="eu-prefix-icon"><mat-icon>business</mat-icon></span>
<mat-select required formControlName="officeId" (selectionChange)="officeChanged($event.value)">
@for (office of officesData; track office) {
<mat-option [value]="office.id">{{ office.name }}</mat-option>
@@ -132,6 +162,7 @@
<mat-form-field appearance="outline" class="eu-field">
<mat-label>{{ 'labels.inputs.Staff' | translate }}</mat-label>
<span matPrefix class="eu-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>
@@ -141,6 +172,7 @@
<mat-form-field appearance="outline" class="eu-field eu-field--full">
<mat-label>{{ 'labels.inputs.Roles' | translate }}</mat-label>
<span matPrefix class="eu-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)
.eu-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)
.eu-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;
}
}
.eu-checkbox-row {
grid-column: 1 / -1;
display: flex;
+10 -1
View File
@@ -14,6 +14,7 @@ import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule }
/** Custom Services */
import { UsersService } from '../users.service';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatIcon } from '@angular/material/icon';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -25,7 +26,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
styleUrls: ['./edit-user.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox
MatCheckbox,
MatIcon
]
})
export class EditUserComponent implements OnInit {
@@ -82,6 +84,13 @@ export class EditUserComponent implements OnInit {
Validators.email
]
],
mobileNo: [
this.userData.mobileNo || '',
[
Validators.required,
Validators.pattern(/^\d{10}$/)
]
],
firstname: [
this.userData.firstname,
[
+11 -13
View File
@@ -60,24 +60,22 @@
<td mat-cell *matCellDef="let user" class="cell-mono">{{ user.email }}</td>
</ng-container>
<ng-container matColumnDef="mobileNo">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Mobile</th>
<td mat-cell *matCellDef="let user" class="cell-mono">
@if (user.mobileNo) {
+91 {{ user.mobileNo }}
} @else {
<span class="cell-empty">—</span>
}
</td>
</ng-container>
<ng-container matColumnDef="officeName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Office' | translate }}</th>
<td mat-cell *matCellDef="let user">{{ user.officeName }}</td>
</ng-container>
<ng-container matColumnDef="isSelfServiceUser">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.buttons.Is Self Service' | translate }}</th>
<td mat-cell *matCellDef="let user">
<span
class="status-badge"
[class.status-active]="user.isSelfServiceUser"
[class.status-inactive]="!user.isSelfServiceUser"
>
{{ user.isSelfServiceUser ? ('labels.buttons.Yes' | translate) : ('labels.buttons.No' | translate) }}
</span>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="table-row"></tr>
</table>
+2 -2
View File
@@ -73,8 +73,8 @@ export class UsersComponent implements OnInit, AfterViewInit {
'firstname',
'lastname',
'email',
'officeName',
'isSelfServiceUser'
'mobileNo',
'officeName'
];
/** Data source for users table. */
dataSource: MatTableDataSource<any>;
@@ -72,6 +72,13 @@
</div>
}
@if (userData.mobileNo) {
<div class="vu-cell">
<div class="vu-cell__label">Mobile</div>
<div class="vu-cell__value">+91 {{ userData.mobileNo }}</div>
</div>
}
<div class="vu-cell">
<div class="vu-cell__label">{{ 'labels.inputs.Roles' | translate }}</div>
<div class="vu-cell__value">