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:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
[
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user