- 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
187 lines
7.1 KiB
HTML
187 lines
7.1 KiB
HTML
<!--
|
|
Copyright since 2025 Mifos Initiative
|
|
|
|
This Source Code Form is subject to the terms of the Mozilla Public
|
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
-->
|
|
|
|
<div class="oal-container">
|
|
<header class="oal-head">
|
|
<div class="oal-head__icon"><fa-icon icon="key"></fa-icon></div>
|
|
<div class="oal-head__text">
|
|
<h2>OTP Audit Logs</h2>
|
|
<p>All OTP send, verify and resend events. Used for auditing MFA and login activity.</p>
|
|
</div>
|
|
<div class="oal-head__buttons">
|
|
<button mat-stroked-button class="oal-head__btn" (click)="downloadCsv()" [disabled]="!dataSource.data.length">
|
|
<mat-icon>download</mat-icon> CSV
|
|
</button>
|
|
<button mat-stroked-button class="oal-head__btn" (click)="refresh()"><mat-icon>refresh</mat-icon> Refresh</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Filter bar -->
|
|
<section class="oal-filters">
|
|
<mat-form-field appearance="outline" class="oal-ff oal-ff--grow">
|
|
<mat-label>Search</mat-label>
|
|
<mat-icon matPrefix>search</mat-icon>
|
|
<input
|
|
matInput
|
|
[(ngModel)]="search"
|
|
placeholder="Username, mobile, email or reference"
|
|
(keydown.enter)="applyFilters()"
|
|
/>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline" class="oal-ff">
|
|
<mat-label>Status</mat-label>
|
|
<mat-select [(ngModel)]="status">
|
|
@for (s of statusOptions; track s.value) {
|
|
<mat-option [value]="s.value">{{ s.label }}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline" class="oal-ff">
|
|
<mat-label>Channel</mat-label>
|
|
<mat-select [(ngModel)]="channel">
|
|
@for (c of channelOptions; track c.value) {
|
|
<mat-option [value]="c.value">{{ c.label }}</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline" class="oal-ff">
|
|
<mat-label>From</mat-label>
|
|
<input matInput [matDatepicker]="fromPicker" [(ngModel)]="dateFrom" />
|
|
<mat-datepicker-toggle matSuffix [for]="fromPicker"></mat-datepicker-toggle>
|
|
<mat-datepicker #fromPicker></mat-datepicker>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline" class="oal-ff">
|
|
<mat-label>To</mat-label>
|
|
<input matInput [matDatepicker]="toPicker" [(ngModel)]="dateTo" />
|
|
<mat-datepicker-toggle matSuffix [for]="toPicker"></mat-datepicker-toggle>
|
|
<mat-datepicker #toPicker></mat-datepicker>
|
|
</mat-form-field>
|
|
|
|
<div class="oal-filters__actions">
|
|
<button mat-flat-button color="primary" (click)="applyFilters()"><mat-icon>filter_list</mat-icon> Apply</button>
|
|
<button mat-stroked-button (click)="clearFilters()"><mat-icon>clear</mat-icon> Clear</button>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Table card -->
|
|
<section class="oal-card">
|
|
@if (loading) {
|
|
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
|
}
|
|
<div class="oal-table-wrap">
|
|
<table mat-table [dataSource]="dataSource" class="oal-table">
|
|
<ng-container matColumnDef="id">
|
|
<th mat-header-cell *matHeaderCellDef>ID</th>
|
|
<td mat-cell *matCellDef="let row" class="oal-mono">#{{ row.id }}</td>
|
|
</ng-container>
|
|
|
|
<ng-container matColumnDef="createdOn">
|
|
<th mat-header-cell *matHeaderCellDef>Created</th>
|
|
<td mat-cell *matCellDef="let row">{{ formatDate(row.createdOn) }}</td>
|
|
</ng-container>
|
|
|
|
<ng-container matColumnDef="username">
|
|
<th mat-header-cell *matHeaderCellDef>User</th>
|
|
<td mat-cell *matCellDef="let row">{{ row.username || '—' }}</td>
|
|
</ng-container>
|
|
|
|
<ng-container matColumnDef="channel">
|
|
<th mat-header-cell *matHeaderCellDef>Channel</th>
|
|
<td mat-cell *matCellDef="let row">
|
|
<span class="oal-chip" [attr.data-channel]="row.channel">
|
|
<mat-icon>{{
|
|
row.channel === 'EMAIL' ? 'mail' : row.channel === 'TOTP' ? 'qr_code_2' : 'smartphone'
|
|
}}</mat-icon>
|
|
{{ row.channel || '—' }}
|
|
</span>
|
|
</td>
|
|
</ng-container>
|
|
|
|
<ng-container matColumnDef="destination">
|
|
<th mat-header-cell *matHeaderCellDef>Destination</th>
|
|
<td mat-cell *matCellDef="let row" class="oal-mono">{{ row.destination || '—' }}</td>
|
|
</ng-container>
|
|
|
|
<ng-container matColumnDef="status">
|
|
<th mat-header-cell *matHeaderCellDef>Status</th>
|
|
<td mat-cell *matCellDef="let row">
|
|
<span class="oal-status" [ngClass]="statusClass(row.status)">{{ row.status || '—' }}</span>
|
|
</td>
|
|
</ng-container>
|
|
|
|
<ng-container matColumnDef="attempts">
|
|
<th mat-header-cell *matHeaderCellDef>Attempts</th>
|
|
<td mat-cell *matCellDef="let row">
|
|
{{ row.attempts ?? 0 }}<span class="oal-dim">{{ row.maxAttempts ? '/' + row.maxAttempts : '' }}</span>
|
|
</td>
|
|
</ng-container>
|
|
|
|
<ng-container matColumnDef="verifiedOn">
|
|
<th mat-header-cell *matHeaderCellDef>Verified</th>
|
|
<td mat-cell *matCellDef="let row">{{ formatDate(row.verifiedOn) }}</td>
|
|
</ng-container>
|
|
|
|
<ng-container matColumnDef="actions">
|
|
<th mat-header-cell *matHeaderCellDef class="oal-actions">Actions</th>
|
|
<td mat-cell *matCellDef="let row" class="oal-actions">
|
|
<button mat-icon-button matTooltip="View details" (click)="view(row)">
|
|
<mat-icon>visibility</mat-icon>
|
|
</button>
|
|
</td>
|
|
</ng-container>
|
|
|
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
|
<tr mat-row *matRowDef="let row; columns: displayedColumns" (click)="view(row)" class="oal-row"></tr>
|
|
</table>
|
|
|
|
@if (!loading && errorStatus) {
|
|
<div class="oal-error">
|
|
<mat-icon>{{ errorStatus === 401 || errorStatus === 403 ? 'lock' : 'error_outline' }}</mat-icon>
|
|
<div>
|
|
<strong>
|
|
@if (errorStatus === 403) {
|
|
Access denied (HTTP 403)
|
|
} @else if (errorStatus === 401) {
|
|
Sign in required (HTTP 401)
|
|
} @else {
|
|
Failed to load (HTTP {{ errorStatus }})
|
|
}
|
|
</strong>
|
|
<span>{{ errorMsg }}</span>
|
|
@if (errorStatus === 403) {
|
|
<span class="oal-error__hint">
|
|
Your role doesn't have permission to read OTP logs. Ask an admin to add the
|
|
<code>READ_OTP_LOG</code> permission to your role under
|
|
<em>Admin → System → Manage Roles and Permissions</em>.
|
|
</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
} @else if (!loading && dataSource.data.length === 0) {
|
|
<div class="oal-empty">
|
|
<mat-icon>inbox</mat-icon>
|
|
<p>No OTP logs match the current filters.</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<mat-paginator
|
|
[length]="totalElements"
|
|
[pageSize]="pageSize"
|
|
[pageSizeOptions]="[10, 25, 50, 100]"
|
|
[pageIndex]="pageIndex"
|
|
(page)="onPage($event)"
|
|
showFirstLastButtons
|
|
></mat-paginator>
|
|
</section>
|
|
</div>
|