feat: DSC enrollment + MFA controls, user CRUD UX, auth error hardening
- DSC module: list/enroll/revoke dialogs and dsc.service (m_bank_user_dsc API) - Access control: DSC + MFA toggles via disable-confirm-dialog - Two-factor auth: expanded UX (DSC + OTP flows) - Users: create/edit form polish, success snackbar on edit, DSC widget retained only inside dsc/ (removed from view-user) - Auth: X-Requested-With added globally; error handler null-safe on empty 500 bodies Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
3b752dc6ae
commit
09fab88451
@@ -0,0 +1,57 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<h2 mat-dialog-title class="revoke-title">
|
||||
<fa-icon icon="ban" class="revoke-title__icon"></fa-icon>
|
||||
Revoke Digital Signature
|
||||
</h2>
|
||||
|
||||
<mat-dialog-content class="revoke-content">
|
||||
<p class="revoke-summary">
|
||||
You're about to revoke
|
||||
<strong>{{ data.dsc.commonName || 'this DSC' }}</strong>
|
||||
(serial {{ data.dsc.dscSerialNoFull || data.dsc.dscSerialNoMasked || '—' }}). This is permanent and the certificate
|
||||
will no longer work for two-factor authentication.
|
||||
</p>
|
||||
|
||||
<mat-form-field class="revoke-reason" appearance="outline">
|
||||
<mat-label>Reason</mat-label>
|
||||
<input
|
||||
matInput
|
||||
name="reason"
|
||||
[(ngModel)]="reason"
|
||||
[disabled]="submitting"
|
||||
maxlength="200"
|
||||
placeholder="e.g. token lost, employee left"
|
||||
required
|
||||
/>
|
||||
@if (!reason?.trim() && submitting === false) {
|
||||
<mat-error>Reason is required</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@if (errorMessage) {
|
||||
<div class="revoke-error">
|
||||
<fa-icon icon="exclamation-circle"></fa-icon>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end" class="revoke-actions">
|
||||
<button mat-stroked-button (click)="cancel()" type="button" [disabled]="submitting">Cancel</button>
|
||||
<button mat-flat-button color="warn" (click)="confirm()" type="button" [disabled]="!reason?.trim() || submitting">
|
||||
@if (submitting) {
|
||||
<mat-spinner diameter="18"></mat-spinner>
|
||||
<span>Revoking…</span>
|
||||
} @else {
|
||||
<fa-icon icon="ban"></fa-icon>
|
||||
<span>Revoke</span>
|
||||
}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.revoke-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #b91c1c;
|
||||
margin: 0;
|
||||
|
||||
&__icon {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
.revoke-content {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.revoke-summary {
|
||||
margin: 0 0 16px;
|
||||
color: #334155;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
|
||||
strong {
|
||||
color: #0f172a;
|
||||
}
|
||||
}
|
||||
|
||||
.revoke-reason {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.revoke-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.revoke-actions {
|
||||
gap: 8px;
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* 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, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogRef,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions
|
||||
} from '@angular/material/dialog';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatFormField, MatLabel, MatError } from '@angular/material/form-field';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { DscService } from '../dsc.service';
|
||||
import { BankUserDsc } from '../dsc.model';
|
||||
|
||||
interface RevokeDialogData {
|
||||
dsc: BankUserDsc;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-revoke-dsc-dialog',
|
||||
templateUrl: './revoke-dsc-dialog.component.html',
|
||||
styleUrls: ['./revoke-dsc-dialog.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FormsModule,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatButton,
|
||||
MatFormField,
|
||||
MatLabel,
|
||||
MatError,
|
||||
MatInput,
|
||||
MatProgressSpinner,
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class RevokeDscDialogComponent {
|
||||
private service = inject(DscService);
|
||||
private dialogRef = inject(MatDialogRef<RevokeDscDialogComponent>);
|
||||
data = inject<RevokeDialogData>(MAT_DIALOG_DATA);
|
||||
|
||||
reason = '';
|
||||
submitting = false;
|
||||
errorMessage = '';
|
||||
|
||||
cancel(): void {
|
||||
this.dialogRef.close({ revoked: false });
|
||||
}
|
||||
|
||||
confirm(): void {
|
||||
if (!this.reason?.trim() || !this.data?.dsc?.id) return;
|
||||
this.submitting = true;
|
||||
this.errorMessage = '';
|
||||
this.service.revokeDsc(this.data.dsc.id, this.reason.trim()).subscribe({
|
||||
next: () => {
|
||||
this.submitting = false;
|
||||
this.dialogRef.close({ revoked: true });
|
||||
},
|
||||
error: (err) => {
|
||||
this.submitting = false;
|
||||
this.errorMessage =
|
||||
err?.error?.defaultUserMessage || err?.error?.message || err?.message || 'Could not revoke DSC.';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user