480 lines
16 KiB
TypeScript
480 lines
16 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/.
|
|
*/
|
|
|
|
/** Angular Imports */
|
|
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
|
|
import { Router } from '@angular/router';
|
|
import { trigger, transition, style, animate } from '@angular/animations';
|
|
|
|
/** rxjs Imports */
|
|
|
|
import { Subscription } from 'rxjs';
|
|
import { take } from 'rxjs/operators';
|
|
/**
|
|
* Interface for version information.
|
|
*/
|
|
export interface VersionInfo {
|
|
tenant?: string;
|
|
mifos?: string;
|
|
fineract?: {
|
|
version?: string;
|
|
};
|
|
}
|
|
|
|
/** Custom Models */
|
|
import { Alert } from '../core/alert/alert.model';
|
|
|
|
/** Custom Services */
|
|
import { AlertService } from '../core/alert/alert.service';
|
|
import { ThemingService } from '../shared/theme-toggle/theming.service';
|
|
|
|
/** Environment Imports */
|
|
import { environment } from '../../environments/environment';
|
|
import { SettingsService } from 'app/settings/settings.service';
|
|
import { LanguageSelectorComponent } from '../shared/language-selector/language-selector.component';
|
|
import { ThemeToggleComponent } from '../shared/theme-toggle/theme-toggle.component';
|
|
import { ServerSelectorComponent } from '../shared/server-selector/server-selector.component';
|
|
import { TenantSelectorComponent } from '../shared/tenant-selector/tenant-selector.component';
|
|
import { LoginFormComponent } from './login-form/login-form.component';
|
|
import { ResetPasswordComponent } from './reset-password/reset-password.component';
|
|
import { TwoFactorAuthenticationComponent } from './two-factor-authentication/two-factor-authentication.component';
|
|
import { AgreementAcceptComponent } from './agreement-accept/agreement-accept.component';
|
|
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
|
import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu';
|
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|
import { M3IconComponent } from '../shared/m3-ui/m3-icon/m3-icon.component';
|
|
|
|
import { VersionService } from '../system/version.service';
|
|
import { AuthenticationService } from '../core/authentication/authentication.service';
|
|
|
|
/**
|
|
* Login component.
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-login',
|
|
templateUrl: './login.component.html',
|
|
styleUrls: ['./login.component.scss'],
|
|
animations: [
|
|
trigger('settingsAnimation', [
|
|
transition(':enter', [
|
|
style({ opacity: 0, transform: 'translateY(-10px) scale(0.95)' }),
|
|
animate('200ms cubic-bezier(0.4, 0, 0.2, 1)', style({ opacity: 1, transform: 'translateY(0) scale(1)' }))
|
|
]),
|
|
transition(':leave', [
|
|
animate('150ms cubic-bezier(0.4, 0, 0.2, 1)', style({ opacity: 0, transform: 'translateY(-10px) scale(0.95)' }))
|
|
])
|
|
]),
|
|
trigger('cardAnimation', [
|
|
transition(':enter', [
|
|
style({ opacity: 0, transform: 'translateY(30px) scale(0.96)' }),
|
|
animate('600ms cubic-bezier(0.4, 0, 0.2, 1)', style({ opacity: 1, transform: 'translateY(0) scale(1)' }))
|
|
])
|
|
])
|
|
],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
LanguageSelectorComponent,
|
|
ThemeToggleComponent,
|
|
ServerSelectorComponent,
|
|
TenantSelectorComponent,
|
|
LoginFormComponent,
|
|
ResetPasswordComponent,
|
|
TwoFactorAuthenticationComponent,
|
|
AgreementAcceptComponent,
|
|
MatMenuTrigger,
|
|
FaIconComponent,
|
|
MatMenu,
|
|
MatMenuItem,
|
|
M3IconComponent
|
|
]
|
|
})
|
|
export class LoginComponent implements OnInit, OnDestroy {
|
|
/** Whether to show the tenant selector dropdown */
|
|
showTenantSelector = true;
|
|
|
|
/** Slider data */
|
|
slides = [
|
|
{
|
|
tag: 'Core Banking',
|
|
icon: '🏦',
|
|
title: 'Real-Time <span class="hl">CBS</span><br>Integration Platform',
|
|
desc: 'Seamless account enquiry, transaction posting & balance fetch via secure host connectivity. Sub-second response with 99.99% uptime SLA.',
|
|
stats: [
|
|
{ value: '<200ms', label: 'Response Time' },
|
|
{ value: '99.99%', label: 'Uptime SLA' },
|
|
{ value: 'ISO 8583', label: 'Protocol' }
|
|
],
|
|
flow: [
|
|
'CBS Host',
|
|
'Middleware API',
|
|
'Account Enquiry',
|
|
'Tx Posting',
|
|
'Balance Fetch'
|
|
]
|
|
},
|
|
{
|
|
tag: 'Fraud Risk Management',
|
|
icon: '',
|
|
title: 'AI-Powered <span class="hl">Fraud Risk</span><br>Management Platform',
|
|
desc: 'Real-time transaction scoring, anomaly detection & automated lien marking. Integrated with MHA CFCFRMS, UN sanctions screening, and rule-based alert engine for instant fraud response.',
|
|
stats: [
|
|
{ value: 'Real-Time', label: 'Tx Scoring' },
|
|
{ value: 'CFCFRMS', label: 'MHA Portal' },
|
|
{ value: 'UN/OFAC', label: 'Sanctions' }
|
|
],
|
|
flow: [
|
|
'Transaction',
|
|
'Risk Engine',
|
|
'Alert & Lien',
|
|
'CFCFRMS API',
|
|
'MHA Portal'
|
|
]
|
|
},
|
|
{
|
|
tag: 'HSM Security',
|
|
icon: '🔐',
|
|
title: 'HSM-Secured <span class="hl">PIN</span><br>& Key Management',
|
|
desc: 'ATM PIN generation, verification & change secured via SafeNet Luna K6 HSM. Full ZPK/PVK/KEK key hierarchy with TR-31 key block.',
|
|
stats: [
|
|
{ value: 'Luna K6', label: 'HSM Model' },
|
|
{ value: 'TR-31', label: 'Key Block' },
|
|
{ value: 'FIPS 140-2', label: 'Certified' }
|
|
],
|
|
flow: [
|
|
'LMK (Master)',
|
|
'ZPK / KEK',
|
|
'PVK / ZEK',
|
|
'PIN Block',
|
|
'VISA PVV'
|
|
]
|
|
},
|
|
{
|
|
tag: 'Cyber Fraud Reporting',
|
|
icon: '🛡️',
|
|
title: '<span class="hl">CFCFRMS</span><br>Fraud Detection & Reporting',
|
|
desc: 'Citizen Financial Cyber Fraud Reporting & Management System with real-time monitoring, MHA portal connectivity, and automated lien marking.',
|
|
stats: [
|
|
{ value: 'MHA', label: 'Portal' },
|
|
{ value: 'Real-Time', label: 'Alerts' },
|
|
{ value: 'Auto Lien', label: 'Mark & Block' }
|
|
],
|
|
flow: [
|
|
'Transaction',
|
|
'Risk Engine',
|
|
'CFCFRMS API',
|
|
'MHA Portal',
|
|
'Lien / Block'
|
|
]
|
|
},
|
|
{
|
|
tag: 'Compliance',
|
|
icon: '📋',
|
|
title: 'RBI & <span class="hl">IDRBT</span><br>Compliant Architecture',
|
|
desc: 'Built in full adherence to RBI circulars and IDRBT security standards — data localisation, audit trails, VAPT-certified APIs and AML screening.',
|
|
stats: [
|
|
{ value: 'IDRBT', label: 'Standards' },
|
|
{ value: 'VAPT', label: 'Certified' },
|
|
{ value: 'AML/OFAC', label: 'Screening' }
|
|
],
|
|
flow: [
|
|
'RBI Circular',
|
|
'IDRBT Std.',
|
|
'Audit Trail',
|
|
'VAPT APIs',
|
|
'AML / OFAC'
|
|
]
|
|
},
|
|
{
|
|
tag: 'CKYC Compliance',
|
|
icon: '🪪',
|
|
title: 'CBS to <span class="hl">CKYC</span><br>Data Submission & Verification',
|
|
desc: 'End-to-end CERSAI CKYC compliance workflow — Non-CKYC identification, auto ZIP packaging, SFTP upload, API-based search & fetch.',
|
|
stats: [
|
|
{ value: 'CERSAI', label: 'Registry' },
|
|
{ value: 'SFTP', label: 'Auto Upload' },
|
|
{ value: 'API Search', label: '& Fetch' }
|
|
],
|
|
flow: [
|
|
'CBS — Non-CKYC',
|
|
'Auto ZIP Pack',
|
|
'SFTP Upload',
|
|
'CERSAI API',
|
|
'Status Update'
|
|
]
|
|
}
|
|
];
|
|
currentSlide = 0;
|
|
slideDuration = 5000;
|
|
private slideTimer: any;
|
|
/** Show version info table if env allows */
|
|
displayBackendInfo = environment.displayBackEndInfo !== 'false';
|
|
/** Production mode - minimal hero with branding only */
|
|
productionMode = environment.productionMode === true;
|
|
|
|
private alertService = inject(AlertService);
|
|
private settingsService = inject(SettingsService);
|
|
private themingService = inject(ThemingService);
|
|
private dialog = inject(MatDialog);
|
|
private twoFactorDialogRef: MatDialogRef<TwoFactorAuthenticationComponent> | null = null;
|
|
private agreementAcceptDialogRef: MatDialogRef<AgreementAcceptComponent> | null = null;
|
|
private router = inject(Router);
|
|
private authService = inject(AuthenticationService);
|
|
|
|
private versionService = inject(VersionService);
|
|
|
|
public environment = environment;
|
|
|
|
/** Version info for display */
|
|
versions: VersionInfo = {};
|
|
/** Server info for display */
|
|
server: string = '';
|
|
|
|
/** Get tenant display name with first letter capitalized */
|
|
get tenantDisplayName(): string {
|
|
const tenant = this.versions?.tenant || this.settingsService.tenantIdentifier || 'default';
|
|
return tenant.charAt(0).toUpperCase() + tenant.slice(1).toLowerCase();
|
|
}
|
|
|
|
/** Whether the settings panel popup is open */
|
|
settingsPanelOpen = false;
|
|
|
|
/** True if password requires a reset. */
|
|
resetPassword = false;
|
|
/** True if user requires two factor authentication. */
|
|
twoFactorAuthenticationRequired = false;
|
|
/** True if the signed-in user still needs to accept the current agreement/disclaimer. */
|
|
agreementAcceptanceRequired = false;
|
|
/** Subscription to alerts. */
|
|
alert$: Subscription;
|
|
logoPath = 'assets/images/cbsnexora-logo.png';
|
|
logoPathDark = 'assets/images/cbsnexora-logo.png';
|
|
/** Subscription to theme changes. */
|
|
theme$: Subscription;
|
|
|
|
themeDarkEnabled: boolean = false;
|
|
|
|
/**
|
|
* Subscribes to alert event of alert service and theme changes.
|
|
*/
|
|
ngOnInit() {
|
|
this.showTenantSelector = this.calculateTenantSelectorVisibility();
|
|
this.startSlider();
|
|
this.updateLogo();
|
|
this.themeDarkEnabled = this.settingsService.themeDarkEnabled;
|
|
// Subscribe to theme changes
|
|
this.theme$ = this.themingService.theme.subscribe((value: string) => {
|
|
this.themeDarkEnabled = this.settingsService.themeDarkEnabled;
|
|
});
|
|
|
|
// Initialize theme based on settings
|
|
this.themingService.setDarkMode(!!this.settingsService.themeDarkEnabled);
|
|
|
|
// Subscribe to alerts
|
|
this.alert$ = this.alertService.alertEvent.subscribe((alertEvent: Alert) => {
|
|
const alertType = alertEvent.type;
|
|
if (alertType === 'Password Expired') {
|
|
this.twoFactorAuthenticationRequired = false;
|
|
this.agreementAcceptanceRequired = false;
|
|
this.closeTwoFactorDialog();
|
|
this.closeAgreementAcceptDialog();
|
|
this.resetPassword = true;
|
|
} else if (alertType === 'Two Factor Authentication Required') {
|
|
this.resetPassword = false;
|
|
this.twoFactorAuthenticationRequired = true;
|
|
this.openTwoFactorDialog();
|
|
} else if (alertType === 'Agreement Acceptance Required') {
|
|
this.resetPassword = false;
|
|
this.agreementAcceptanceRequired = true;
|
|
this.openAgreementAcceptDialog();
|
|
} else if (alertType === 'Authentication Success') {
|
|
this.resetPassword = false;
|
|
this.twoFactorAuthenticationRequired = false;
|
|
this.agreementAcceptanceRequired = false;
|
|
this.closeTwoFactorDialog();
|
|
this.closeAgreementAcceptDialog();
|
|
// Cashier users go directly to the full-screen transaction entry dashboard
|
|
const destination = this.authService.isCashierUser() ? ['/cashier/transaction-entry'] : ['/'];
|
|
this.router.navigate(destination, { replaceUrl: true });
|
|
} else if (alertType === 'Tenant Changed') {
|
|
this.updateLogo();
|
|
}
|
|
});
|
|
|
|
// Load version info for table
|
|
this.versionService
|
|
.getBackendInfo()
|
|
.pipe(take(1))
|
|
.subscribe(
|
|
(info: any) => {
|
|
this.versions = {
|
|
tenant: this.settingsService.tenantIdentifier,
|
|
mifos: info?.mifos || info?.mifosX || info?.mifos_x || info?.version || environment.version,
|
|
fineract:
|
|
typeof info?.fineract === 'object' && info?.fineract !== null
|
|
? { version: info.fineract.version }
|
|
: typeof info?.fineract === 'string'
|
|
? { version: info.fineract }
|
|
: info?.fineractX || info?.fineract_x
|
|
? { version: info.fineractX || info.fineract_x }
|
|
: { version: info?.git?.build?.version }
|
|
};
|
|
},
|
|
() => {
|
|
this.versions = {
|
|
tenant: this.settingsService.tenantIdentifier,
|
|
mifos: environment.version,
|
|
fineract: { version: '' }
|
|
};
|
|
}
|
|
);
|
|
this.server = this.settingsService.server;
|
|
}
|
|
|
|
/**
|
|
* Unsubscribes from alerts and theme changes.
|
|
*/
|
|
ngOnDestroy() {
|
|
if (this.alert$) {
|
|
this.alert$.unsubscribe();
|
|
}
|
|
if (this.theme$) {
|
|
this.theme$.unsubscribe();
|
|
}
|
|
this.closeTwoFactorDialog();
|
|
this.closeAgreementAcceptDialog();
|
|
clearInterval(this.slideTimer);
|
|
}
|
|
|
|
/** Open the 2FA challenge as a non-dismissable modal */
|
|
private openTwoFactorDialog(): void {
|
|
if (this.twoFactorDialogRef) return;
|
|
this.twoFactorDialogRef = this.dialog.open(TwoFactorAuthenticationComponent, {
|
|
width: '440px',
|
|
maxWidth: '95vw',
|
|
panelClass: 'tfa-dialog-panel',
|
|
backdropClass: 'tfa-dialog-backdrop',
|
|
disableClose: true,
|
|
autoFocus: 'first-tabbable',
|
|
restoreFocus: true
|
|
});
|
|
this.twoFactorDialogRef.afterClosed().subscribe(() => {
|
|
this.twoFactorDialogRef = null;
|
|
});
|
|
}
|
|
|
|
private closeTwoFactorDialog(): void {
|
|
if (this.twoFactorDialogRef) {
|
|
this.twoFactorDialogRef.close();
|
|
this.twoFactorDialogRef = null;
|
|
}
|
|
}
|
|
|
|
/** Open the post-login agreement acceptance dialog as a non-dismissable modal */
|
|
private openAgreementAcceptDialog(): void {
|
|
if (this.agreementAcceptDialogRef) return;
|
|
this.agreementAcceptDialogRef = this.dialog.open(AgreementAcceptComponent, {
|
|
width: '460px',
|
|
maxWidth: '95vw',
|
|
panelClass: 'tfa-dialog-panel',
|
|
backdropClass: 'tfa-dialog-backdrop',
|
|
disableClose: true,
|
|
autoFocus: 'first-tabbable',
|
|
restoreFocus: true
|
|
});
|
|
this.agreementAcceptDialogRef.afterClosed().subscribe(() => {
|
|
this.agreementAcceptDialogRef = null;
|
|
});
|
|
}
|
|
|
|
private closeAgreementAcceptDialog(): void {
|
|
if (this.agreementAcceptDialogRef) {
|
|
this.agreementAcceptDialogRef.close();
|
|
this.agreementAcceptDialogRef = null;
|
|
}
|
|
}
|
|
|
|
goToSlide(index: number): void {
|
|
this.currentSlide = index;
|
|
this.restartSlider();
|
|
}
|
|
|
|
private startSlider(): void {
|
|
this.slideTimer = setInterval(() => {
|
|
this.currentSlide = (this.currentSlide + 1) % this.slides.length;
|
|
}, this.slideDuration);
|
|
}
|
|
|
|
private restartSlider(): void {
|
|
clearInterval(this.slideTimer);
|
|
this.startSlider();
|
|
}
|
|
|
|
toggleSettingsPanel(): void {
|
|
this.settingsPanelOpen = !this.settingsPanelOpen;
|
|
}
|
|
|
|
reloadSettings(): void {
|
|
this.settingsService.setTenantIdentifier('');
|
|
this.settingsService.setTenantIdentifier(environment.fineractPlatformTenantId || 'default');
|
|
this.settingsService.setTenantIdentifiers(environment.fineractPlatformTenantIds.split(','));
|
|
this.settingsService.setServers(environment.baseApiUrls.split(','));
|
|
window.location.reload();
|
|
}
|
|
|
|
private calculateTenantSelectorVisibility(): boolean {
|
|
if (environment.oauth.enabled) {
|
|
return false;
|
|
}
|
|
if (environment.displayTenantSelector === 'false') {
|
|
return false;
|
|
}
|
|
const tenantIds = environment.fineractPlatformTenantIds
|
|
.split(',')
|
|
.map((id) => id.trim())
|
|
.filter((id) => id.length > 0);
|
|
if (tenantIds.length === 0) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
allowServerSwitch(): boolean {
|
|
return environment.allowServerSwitch === 'false' ? false : true;
|
|
}
|
|
|
|
updateLogo(): void {
|
|
const tenant = this.settingsService.tenantIdentifier;
|
|
const isTenantSpecific = tenant && tenant !== 'default';
|
|
|
|
// Set light mode logo (env override takes priority)
|
|
if (environment.tenantLogoUrl && environment.tenantLogoUrl.trim() !== '') {
|
|
this.logoPath = environment.tenantLogoUrl;
|
|
} else {
|
|
this.logoPath = isTenantSpecific ? `assets/images/${tenant}_home.png` : 'assets/images/cbsnexora-logo.png';
|
|
}
|
|
|
|
// Set dark mode logo (env override takes priority)
|
|
if (environment.tenantLogoUrlDark && environment.tenantLogoUrlDark.trim() !== '') {
|
|
this.logoPathDark = environment.tenantLogoUrlDark;
|
|
} else {
|
|
this.logoPathDark = isTenantSpecific
|
|
? `assets/images/${tenant}_home_dark.png`
|
|
: 'assets/images/cbsnexora-logo.png';
|
|
}
|
|
}
|
|
|
|
onLogoError(): void {
|
|
this.logoPath = 'assets/images/cbsnexora-logo.png';
|
|
}
|
|
|
|
onLogoErrorDark(): void {
|
|
this.logoPathDark = 'assets/images/cbsnexora-logo.png';
|
|
}
|
|
}
|