/** * 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 CBS
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 Fraud Risk
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 PIN
& 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: 'CFCFRMS
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 & IDRBT
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 CKYC
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 | null = null; private agreementAcceptDialogRef: MatDialogRef | 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'; } }