- 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
291 lines
7.6 KiB
TypeScript
291 lines
7.6 KiB
TypeScript
/**
|
|
* 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/.
|
|
*/
|
|
|
|
import { Component, OnInit, ViewChild, inject } from '@angular/core';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { Router, RouterLink } from '@angular/router';
|
|
import {
|
|
MatTableDataSource,
|
|
MatTable,
|
|
MatColumnDef,
|
|
MatHeaderCellDef,
|
|
MatHeaderCell,
|
|
MatCellDef,
|
|
MatCell,
|
|
MatHeaderRowDef,
|
|
MatHeaderRow,
|
|
MatRowDef,
|
|
MatRow
|
|
} from '@angular/material/table';
|
|
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
|
import { MatSort } from '@angular/material/sort';
|
|
import { MatProgressBar } from '@angular/material/progress-bar';
|
|
import { MatButton, MatIconButton } from '@angular/material/button';
|
|
import { MatIcon } from '@angular/material/icon';
|
|
import { MatFormField, MatLabel, MatPrefix } from '@angular/material/form-field';
|
|
import { MatInput } from '@angular/material/input';
|
|
import { MatSelect } from '@angular/material/select';
|
|
import { MatOption } from '@angular/material/core';
|
|
import { MatDatepicker, MatDatepickerInput, MatDatepickerToggle } from '@angular/material/datepicker';
|
|
import { MatNativeDateModule } from '@angular/material/core';
|
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|
import { OtpAuditLogsService, OtpLog, OtpLogPage } from './otp-audit-logs.service';
|
|
|
|
@Component({
|
|
selector: 'mifosx-otp-audit-logs',
|
|
templateUrl: './otp-audit-logs.component.html',
|
|
styleUrls: ['./otp-audit-logs.component.scss'],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
FormsModule,
|
|
RouterLink,
|
|
MatTable,
|
|
MatColumnDef,
|
|
MatHeaderCellDef,
|
|
MatHeaderCell,
|
|
MatCellDef,
|
|
MatCell,
|
|
MatHeaderRowDef,
|
|
MatHeaderRow,
|
|
MatRowDef,
|
|
MatRow,
|
|
MatPaginator,
|
|
MatProgressBar,
|
|
MatButton,
|
|
MatIconButton,
|
|
MatIcon,
|
|
MatFormField,
|
|
MatLabel,
|
|
MatPrefix,
|
|
MatInput,
|
|
MatSelect,
|
|
MatOption,
|
|
MatDatepicker,
|
|
MatDatepickerInput,
|
|
MatDatepickerToggle,
|
|
MatNativeDateModule,
|
|
FaIconComponent
|
|
]
|
|
})
|
|
export class OtpAuditLogsComponent implements OnInit {
|
|
private service = inject(OtpAuditLogsService);
|
|
private router = inject(Router);
|
|
|
|
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
|
|
|
loading = false;
|
|
totalElements = 0;
|
|
dataSource = new MatTableDataSource<OtpLog>([]);
|
|
/** Last error returned by the listing endpoint (if any) */
|
|
errorStatus = 0;
|
|
errorMsg = '';
|
|
|
|
displayedColumns: string[] = [
|
|
'id',
|
|
'createdOn',
|
|
'username',
|
|
'channel',
|
|
'destination',
|
|
'status',
|
|
'attempts',
|
|
'verifiedOn',
|
|
'actions'
|
|
];
|
|
|
|
/** Filter state */
|
|
search = '';
|
|
status = '';
|
|
channel = '';
|
|
dateFrom: Date | null = null;
|
|
dateTo: Date | null = null;
|
|
pageSize = 25;
|
|
pageIndex = 0;
|
|
|
|
readonly statusOptions = [
|
|
{ value: '', label: 'All' },
|
|
{ value: 'PENDING', label: 'Pending' },
|
|
{ value: 'VERIFIED', label: 'Verified' },
|
|
{ value: 'FAILED', label: 'Failed' },
|
|
{ value: 'EXPIRED', label: 'Expired' },
|
|
{ value: 'LOCKED', label: 'Locked' }
|
|
];
|
|
|
|
readonly channelOptions = [
|
|
{ value: '', label: 'All' },
|
|
{ value: 'SMS', label: 'SMS' },
|
|
{ value: 'EMAIL', label: 'Email' },
|
|
{ value: 'TOTP', label: 'Authenticator' }
|
|
];
|
|
|
|
ngOnInit(): void {
|
|
this.fetch();
|
|
}
|
|
|
|
fetch(): void {
|
|
this.loading = true;
|
|
this.errorStatus = 0;
|
|
this.errorMsg = '';
|
|
this.service
|
|
.list({
|
|
page: this.pageIndex,
|
|
pageSize: this.pageSize,
|
|
search: this.search?.trim() || undefined,
|
|
status: this.status || undefined,
|
|
channel: this.channel || undefined,
|
|
dateFrom: this.toIso(this.dateFrom),
|
|
dateTo: this.toIso(this.dateTo)
|
|
})
|
|
.subscribe({
|
|
next: (res: OtpLogPage | OtpLog[]) => {
|
|
this.loading = false;
|
|
if (Array.isArray(res)) {
|
|
this.totalElements = res.length;
|
|
this.dataSource.data = res;
|
|
} else {
|
|
this.totalElements = res?.totalElements ?? res?.pageItems?.length ?? 0;
|
|
this.dataSource.data = res?.pageItems || [];
|
|
}
|
|
},
|
|
error: (err) => {
|
|
this.loading = false;
|
|
this.dataSource.data = [];
|
|
this.totalElements = 0;
|
|
this.errorStatus = err?.status || 0;
|
|
this.errorMsg =
|
|
err?.error?.defaultUserMessage || err?.error?.message || err?.message || 'Failed to load OTP audit logs.';
|
|
}
|
|
});
|
|
}
|
|
|
|
applyFilters(): void {
|
|
this.pageIndex = 0;
|
|
if (this.paginator) this.paginator.firstPage();
|
|
this.fetch();
|
|
}
|
|
|
|
clearFilters(): void {
|
|
this.search = '';
|
|
this.status = '';
|
|
this.channel = '';
|
|
this.dateFrom = null;
|
|
this.dateTo = null;
|
|
this.applyFilters();
|
|
}
|
|
|
|
onPage(e: PageEvent): void {
|
|
this.pageIndex = e.pageIndex;
|
|
this.pageSize = e.pageSize;
|
|
this.fetch();
|
|
}
|
|
|
|
view(log: OtpLog): void {
|
|
if (log?.id != null)
|
|
this.router.navigate([
|
|
'/otp-audit-logs',
|
|
log.id
|
|
]);
|
|
}
|
|
|
|
refresh(): void {
|
|
this.fetch();
|
|
}
|
|
|
|
/** Export the currently displayed page as a CSV file. */
|
|
downloadCsv(): void {
|
|
const rows = this.dataSource.data || [];
|
|
if (!rows.length) return;
|
|
const headers = [
|
|
'ID',
|
|
'Created',
|
|
'User',
|
|
'Channel',
|
|
'Destination',
|
|
'Status',
|
|
'Purpose',
|
|
'Attempts',
|
|
'Verified On',
|
|
'IP',
|
|
'Reference'
|
|
];
|
|
const escape = (v: any) => {
|
|
if (v == null) return '';
|
|
const s = String(v).replace(/"/g, '""');
|
|
return /[",\n]/.test(s) ? `"${s}"` : s;
|
|
};
|
|
const lines = [headers.join(',')];
|
|
for (const r of rows) {
|
|
lines.push(
|
|
[
|
|
r.id,
|
|
this.formatDate(r.createdOn),
|
|
r.username,
|
|
r.channel,
|
|
r.destination,
|
|
r.status,
|
|
r.purpose,
|
|
r.attempts,
|
|
this.formatDate(r.verifiedOn),
|
|
r.ipAddress,
|
|
r.otpReference
|
|
]
|
|
.map(escape)
|
|
.join(',')
|
|
);
|
|
}
|
|
const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8;' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `otp-audit-logs-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
/** Format date arrays/strings into a friendly local string */
|
|
formatDate(value: any): string {
|
|
if (!value) return '—';
|
|
if (Array.isArray(value) && value.length >= 3) {
|
|
const [
|
|
y,
|
|
m,
|
|
d,
|
|
h = 0,
|
|
mi = 0,
|
|
s = 0
|
|
] = value;
|
|
const dt = new Date(y, m - 1, d, h, mi, s);
|
|
return dt.toLocaleString();
|
|
}
|
|
if (typeof value === 'string') {
|
|
const dt = new Date(value);
|
|
return isNaN(dt.getTime()) ? value : dt.toLocaleString();
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
statusClass(status?: string): string {
|
|
if (!status) return '';
|
|
const s = status.toUpperCase();
|
|
if (s === 'VERIFIED') return 'is-ok';
|
|
if (s === 'PENDING') return 'is-warn';
|
|
if (s === 'FAILED' || s === 'EXPIRED' || s === 'LOCKED') return 'is-bad';
|
|
return '';
|
|
}
|
|
|
|
private toIso(d: Date | null): string | undefined {
|
|
if (!d) return undefined;
|
|
const yyyy = d.getFullYear();
|
|
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
const dd = String(d.getDate()).padStart(2, '0');
|
|
return `${yyyy}-${mm}-${dd}`;
|
|
}
|
|
}
|