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:
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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 { Injectable, inject } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ImageCropperDialogComponent } from '../clients-view/custom-dialogs/image-cropper-dialog/image-cropper-dialog.component';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class KycUploadHelperService {
|
||||
private dialog = inject(MatDialog);
|
||||
|
||||
/**
|
||||
* If the picked file is an image, open the cropper dialog and resolve with
|
||||
* the cropped File (or null if cancelled). Non-image files (PDFs) are
|
||||
* passed through unchanged.
|
||||
*/
|
||||
pickWithCrop(file: File, title: string): Promise<File | null> {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
return Promise.resolve(file);
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const ref = this.dialog.open(ImageCropperDialogComponent, {
|
||||
width: '720px',
|
||||
maxWidth: '95vw',
|
||||
data: { file, title }
|
||||
});
|
||||
ref.afterClosed().subscribe((cropped: File | null) => resolve(cropped || null));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stack two images (front above back) into one PNG. Resolves with both
|
||||
* the new File (suitable for upload) and a data-URL preview for the UI.
|
||||
* Returns null if either side is not an image (e.g. one is a PDF).
|
||||
*/
|
||||
mergeFrontBack(front: File, back: File, name: string): Promise<{ file: File; preview: string } | null> {
|
||||
if (!front.type.startsWith('image/') || !back.type.startsWith('image/')) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return Promise.all([
|
||||
this.fileToImage(front),
|
||||
this.fileToImage(back)
|
||||
]).then(
|
||||
([
|
||||
fImg,
|
||||
bImg
|
||||
]) => {
|
||||
const targetWidth = Math.max(fImg.width, bImg.width);
|
||||
const fScale = targetWidth / fImg.width;
|
||||
const bScale = targetWidth / bImg.width;
|
||||
const fH = fImg.height * fScale;
|
||||
const bH = bImg.height * bScale;
|
||||
const gap = 20;
|
||||
const totalH = Math.round(fH + bH + gap);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = totalH;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return null;
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(fImg, 0, 0, targetWidth, fH);
|
||||
ctx.drawImage(bImg, 0, fH + gap, targetWidth, bH);
|
||||
|
||||
return new Promise<{ file: File; preview: string } | null>((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
const file = new File([blob], `${name}-combined.png`, { type: 'image/png' });
|
||||
const preview = canvas.toDataURL('image/png');
|
||||
resolve({ file, preview });
|
||||
}, 'image/png');
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private fileToImage(file: File): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(file);
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
resolve(img);
|
||||
};
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error('image load failed'));
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user