Files
cbsnexora/src/app/login/login.component.ts
T

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';
}
}