From bfd412bbc9836d0ae09d7199dd810b48715190ba Mon Sep 17 00:00:00 2001 From: sudeep Date: Thu, 14 Dec 2023 11:00:44 +0530 Subject: [PATCH] MOSIP-24690 : added popup for session idle logout Signed-off-by: sudeep --- package-lock.json | 15 ++ package.json | 1 + src/app/app-config.service.ts | 14 ++ src/app/app.component.ts | 26 ++- src/app/app.constants.ts | 5 + .../components/dialog/dialog.component.css | 6 + .../components/dialog/dialog.component.html | 13 ++ .../components/dialog/dialog.component.ts | 12 +- src/app/core/services/logout.service.ts | 1 + .../services/session-logout.service.spec.ts | 16 ++ .../core/services/session-logout.service.ts | 161 ++++++++++++++++++ .../projects-dashboard.component.ts | 23 ++- src/assets/i18n/ara.json | 4 +- src/assets/i18n/eng.json | 4 +- src/assets/i18n/fra.json | 4 +- 15 files changed, 297 insertions(+), 8 deletions(-) create mode 100644 src/app/core/services/session-logout.service.spec.ts create mode 100644 src/app/core/services/session-logout.service.ts diff --git a/package-lock.json b/package-lock.json index 38b61a2e..16f1416e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6462,6 +6462,21 @@ "tslib": "^2.0.0" } }, + "angular-user-idle": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/angular-user-idle/-/angular-user-idle-2.2.7.tgz", + "integrity": "sha512-Vu+vioYIRwT/Y2Aq+qXhP+wrleAfU50FIix736CY0IW37yAjRNPuXVljgRRtBSildVIzLRVtaGTqvC44duDo2w==", + "requires": { + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", diff --git a/package.json b/package.json index 1dda3981..e50fc40c 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@stomp/rx-stomp": "^2.0.0", "@stomp/stompjs": "^7.0.0", "angular-cd-timer": "^1.3.0", + "angular-user-idle": "^2.2.7", "base64-js": "1.3.0", "crypto-js": "^4.2.0", "js-sha256": "0.9.0", diff --git a/src/app/app-config.service.ts b/src/app/app-config.service.ts index 9c4de8a9..f68432a7 100644 --- a/src/app/app-config.service.ts +++ b/src/app/app-config.service.ts @@ -23,6 +23,7 @@ export class AppConfigService { (response: any) => { //console.log(response); this.appConfig = { ...this.appConfig, ...response["response"] }; + this.setConfig(this.appConfig); //console.log(this.appConfig); }, (error) => { @@ -31,6 +32,19 @@ export class AppConfigService { ); } + public setConfig(configJson: any) { + localStorage.setItem('config', JSON.stringify(configJson)); + } + + public getConfigByKey(key: string) { + const configString = localStorage.getItem('config'); + if (configString) { + const config = JSON.parse(configString); + return config[key]; + } + return null; + } + getConfig() { return this.appConfig; } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 01332bf4..77383061 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, HostListener } from '@angular/core'; +import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; import { RouterEvent, NavigationStart, @@ -9,6 +9,8 @@ import { } from '@angular/router'; import { filter } from 'rxjs'; import { AppConfigService } from './app-config.service'; +import { SessionLogoutService } from './core/services/session-logout.service'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-root', @@ -23,9 +25,13 @@ export class AppComponent { subscribed: any; + subscriptions: Subscription[] = []; + + constructor( private router: Router, private appConfigService: AppConfigService, + private sessionLogoutService: SessionLogoutService ) { this.subscribed = router.events.subscribe(event => { @@ -34,6 +40,11 @@ export class AppComponent { } + ngOnInit() { + this.subscriptions.push(this.sessionLogoutService.currentMessageAutoLogout.subscribe(() => { })); + this.sessionLogoutService.changeMessage({ timerFired: false }); + } + navigationInterceptor(event: any): void { if (event instanceof NavigationStart) { //console.log("NavigationStart"); @@ -52,4 +63,17 @@ export class AppComponent { this.loading = false; } } + + @HostListener('mouseover') + @HostListener('keypress') + @HostListener('click') + @HostListener('document:keypress', ['$event']) + @HostListener('document:mousemove', ['$event']) + onMouseClick() { + this.sessionLogoutService.setisActive(true); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(subscription => subscription.unsubscribe()); + } } diff --git a/src/app/app.constants.ts b/src/app/app.constants.ts index 4a6fcf22..6780e4f2 100644 --- a/src/app/app.constants.ts +++ b/src/app/app.constants.ts @@ -103,3 +103,8 @@ export const REPORT_STATUS_APPROVED = "approved"; export const REPORT_STATUS_REJECTED = "rejected"; export const NO_DATA_AVAILABLE = 'no_data_available'; export const LOADING = 'loading'; + +//session idle values from config +export const SESSION_LOGOUT_IDLE = "sessionLogoutIdle"; +export const SESSION_LOGOUT_TIMEOUT = 'sessionLogoutTimer'; +export const SESSION_LOGOUT_PING = 'sessionLogoutPing'; diff --git a/src/app/core/components/dialog/dialog.component.css b/src/app/core/components/dialog/dialog.component.css index 24339896..b1331331 100644 --- a/src/app/core/components/dialog/dialog.component.css +++ b/src/app/core/components/dialog/dialog.component.css @@ -20,6 +20,12 @@ mat-dialog-content { vertical-align: middle; } +.ok-button-container { + display: flex; + align-items: center; + justify-content: flex-end; +} + .formContainer { background-color: white; margin-top: 10px; diff --git a/src/app/core/components/dialog/dialog.component.html b/src/app/core/components/dialog/dialog.component.html index 229e1cc1..e54a9543 100644 --- a/src/app/core/components/dialog/dialog.component.html +++ b/src/app/core/components/dialog/dialog.component.html @@ -305,4 +305,17 @@

{{"dialogMessages.addProject"|tran +
+ +

{{ 'dialogMessages.sessionInactiveMessage' | translate }}

+
+
+
+ +

{{ 'dialogMessages.sessionInactivityLogoutMessage' | translate }}

+
+ + + +
diff --git a/src/app/core/components/dialog/dialog.component.ts b/src/app/core/components/dialog/dialog.component.ts index ae83745a..f001ba43 100644 --- a/src/app/core/components/dialog/dialog.component.ts +++ b/src/app/core/components/dialog/dialog.component.ts @@ -13,6 +13,7 @@ import { Router } from '@angular/router'; import { Subscription, flatMap } from 'rxjs'; import Utils from 'src/app/app.utils'; import { AppConfigService } from 'src/app/app-config.service'; +import { LogoutService } from '../../services/logout.service'; @Component({ selector: 'app-dialog', @@ -59,8 +60,9 @@ export class DialogComponent implements OnInit { private dataService: DataService, private dialog: MatDialog, private translate: TranslateService, - private userProfileService: UserProfileService - ) { + private userProfileService: UserProfileService, + private logoutservice: LogoutService + ) { dialogRef.disableClose = true; } @@ -466,4 +468,10 @@ export class DialogComponent implements OnInit { checkHashAndWebsiteUrl() { this.dialogRef.close(false); } + + // Close the dialog when the user clicks on the "OK" button, and initiate the logout process. + onOkClick(): void { + this.dialogRef.close(); + this.logoutservice.logout(); + } } diff --git a/src/app/core/services/logout.service.ts b/src/app/core/services/logout.service.ts index c86e6d63..7bb18aac 100644 --- a/src/app/core/services/logout.service.ts +++ b/src/app/core/services/logout.service.ts @@ -26,6 +26,7 @@ export class LogoutService { return true; } else { window.location.href = `${this.appService.getConfig().SERVICES_BASE_URL}${this.appService.getConfig().logout}?redirecturi=` + btoa(window.location.href); + localStorage.removeItem('config'); } //let adminUrl = this.appService.getConfig().toolkitUiUrl; /* diff --git a/src/app/core/services/session-logout.service.spec.ts b/src/app/core/services/session-logout.service.spec.ts new file mode 100644 index 00000000..07a54c94 --- /dev/null +++ b/src/app/core/services/session-logout.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SessionLogoutService } from './session-logout.service'; + +describe('SessionLogoutService', () => { + let service: SessionLogoutService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SessionLogoutService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/services/session-logout.service.ts b/src/app/core/services/session-logout.service.ts new file mode 100644 index 00000000..30a9c249 --- /dev/null +++ b/src/app/core/services/session-logout.service.ts @@ -0,0 +1,161 @@ +import { Injectable } from '@angular/core'; +import { UserIdleService, UserIdleConfig } from 'angular-user-idle'; +import { AuthService } from './authservice.service'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogComponent } from '../components/dialog/dialog.component'; +import { BehaviorSubject } from 'rxjs'; +import { AppConfigService } from 'src/app/app-config.service'; +import * as appConstants from 'src/app/app.constants'; +import { LogoutService } from './logout.service'; +import { environment } from 'src/environments/environment'; + +/** + * @description This class is responsible for auto logging out user when he is inactive for a + * specified period of time. + */ + +@Injectable({ + providedIn: 'root' +}) +export class SessionLogoutService { + private messageAutoLogout = new BehaviorSubject({}); + currentMessageAutoLogout = this.messageAutoLogout.asObservable(); + isActive = false; + timer = new UserIdleConfig(); + isAndroidAppMode = environment.isAndroidAppMode == 'yes' ? true : false; + + idle: number; + timeout: number; + ping: number; + dialogref: any; + dialogreflogout: any; + + constructor( + private userIdle: UserIdleService, + private authService: AuthService, + private dialog: MatDialog, + private configservice: AppConfigService, + private logoutservice: LogoutService + ) { } + + /** + * @description This method gets value of idle,timeout and ping parameter from config file. + */ + getValues() { + // Convert minutes to seconds for idle, timeout, and use the ping value as it is in seconds. + (this.idle = Number( + this.configservice.getConfigByKey(appConstants.SESSION_LOGOUT_IDLE) * 60 + )), + (this.timeout = Number( + this.configservice.getConfigByKey(appConstants.SESSION_LOGOUT_TIMEOUT) * 60 + )), + (this.ping = Number( + this.configservice.getConfigByKey(appConstants.SESSION_LOGOUT_PING) + )); + } + + setisActive(value: boolean) { + this.isActive = value; + } + getisActive() { + return this.isActive; + } + + changeMessage(message: object) { + this.messageAutoLogout.next(message); + } + + /** + * @description This method sets value of idle,timeout and ping parameter from config file. + */ + setValues() { + this.userIdle.stopWatching(); + this.timer.idle = this.idle; + this.timer.ping = this.ping; + this.timer.timeout = this.timeout; + this.userIdle.setConfigValues(this.timer); + } + + /** + * @description This method is fired when dashboard gets loaded and starts the timer to watch for + * user idle. onTimerStart() is fired when user idle has been detected for specified time. + * After that onTimeout() is fired. + */ + + public keepWatching() { + this.userIdle.startWatching(); + this.changeMessage({ timerFired: true }); + + this.userIdle.onTimerStart().subscribe( + (res) => { + if (res === 1) { + this.setisActive(false); + this.openPopUp(); + } else { + if (this.isActive) { + if (this.dialogref) this.dialogref.close(); + this.userIdle.resetTimer(); + } + } + }, + () => {}, + () => {} + ); + + this.userIdle.onTimeout().subscribe(() => { + if (!this.isActive) { + console.log(this.isAndroidAppMode); + if (!this.isAndroidAppMode) { + this.onLogOut(); + } else { + this.dialogref.close(); + this.userIdle.stopWatching(); + } + } else { + this.userIdle.resetTimer(); + } + }); + } + + public continueWatching() { + this.userIdle.startWatching(); + } + /** + * @description This method is used to logged out the user. + */ + onLogOut() { + this.dialogref.close(); + this.dialog.closeAll(); + this.popUpPostLogOut(); + this.userIdle.stopWatching(); + + /// After displaying the session logout popup for five seconds, initiate the logout process. + setTimeout(() => { + this.logoutservice.logout(); + }, 5000); + } + + + /** + * @description This method opens a pop up when user idle has been detected for given time.id + */ + + openPopUp() { + const data = { + case: 'SESSION_TIMEOUT_POPUP', + }; + this.dialogref = this.dialog.open(DialogComponent, { + width: '500px', + data: data + }); + } + popUpPostLogOut() { + const data = { + case: 'POSTLOGOUT_POPUP', + }; + this.dialogreflogout = this.dialog.open(DialogComponent, { + width: '500px', + data: data + }); + } +} diff --git a/src/app/features/dashboard/projects-dashboard/projects-dashboard.component.ts b/src/app/features/dashboard/projects-dashboard/projects-dashboard.component.ts index 44d8604d..09dec52c 100644 --- a/src/app/features/dashboard/projects-dashboard/projects-dashboard.component.ts +++ b/src/app/features/dashboard/projects-dashboard/projects-dashboard.component.ts @@ -14,6 +14,7 @@ import { BreadcrumbService } from 'xng-breadcrumb'; import { DialogComponent } from 'src/app/core/components/dialog/dialog.component'; import { AppConfigService } from 'src/app/app-config.service'; import { Subscription } from 'rxjs'; +import { SessionLogoutService } from 'src/app/core/services/session-logout.service'; export interface ProjectData { id: string; @@ -52,6 +53,8 @@ export class ProjectsDashboardComponent implements OnInit { buttonPosition: any = this.textDirection == 'rtl' ? {'float': 'left'} : {'float': 'right'}; resourceBundleJson: any = {}; isAdmin: boolean = false; + message:any = {}; + constructor( private appConfigService: AppConfigService, private router: Router, @@ -59,7 +62,8 @@ export class ProjectsDashboardComponent implements OnInit { private breadcrumbService: BreadcrumbService, private dialog: MatDialog, private userProfileService: UserProfileService, - private dataService: DataService + private dataService: DataService, + private sessionLogoutService: SessionLogoutService ) { } @@ -78,6 +82,22 @@ export class ProjectsDashboardComponent implements OnInit { } this.dataSource.sort = this.sort; this.dataLoaded = true; + this.sessionIdleTimeout(); + } + + sessionIdleTimeout() { + const subs = this.sessionLogoutService.currentMessageAutoLogout.subscribe( + (message) => (this.message = message) + ); + this.subscriptions.push(subs); + if (!this.message["timerFired"]) { + this.sessionLogoutService.getValues(); + this.sessionLogoutService.setValues(); + this.sessionLogoutService.keepWatching(); + } else { + this.sessionLogoutService.getValues(); + this.sessionLogoutService.continueWatching(); + } } initBreadCrumb() { @@ -98,7 +118,6 @@ export class ProjectsDashboardComponent implements OnInit { this.dataService.getProjects(projectType).subscribe( (response: any) => { (async () => { - console.log(response); let dataArr = response['response']['projects']; let tableData = []; for (let row of dataArr) { diff --git a/src/assets/i18n/ara.json b/src/assets/i18n/ara.json index c0780c6d..4a72e3d9 100644 --- a/src/assets/i18n/ara.json +++ b/src/assets/i18n/ara.json @@ -38,7 +38,9 @@ "projectName": "اسم المشروع", "addProject": "أضف المشروع", "verifyMessage": "تحقق من قيم التجزئة وموقع الويب قبل المتابعة. إذا كانت القيم صحيحة، فاحفظ المشروع.", - "saveBtn": "يحفظ" + "saveBtn": "يحفظ", + "sessionInactiveMessage": "سيتم انتهاء المهلة خلال 60 ثانية بسبب عدم النشاط. الرجاء النقر في أي مكان لإعادة تنشيط جلستك", + "sessionInactivityLogoutMessage": "لقد تم تسجيل الخروج بسبب عدم النشاط." }, "breadcrumb": { "home": "بيت", diff --git a/src/assets/i18n/eng.json b/src/assets/i18n/eng.json index d6910e51..8cdeeabb 100644 --- a/src/assets/i18n/eng.json +++ b/src/assets/i18n/eng.json @@ -38,7 +38,9 @@ "projectName": "Project Name", "addProject": "Add Project", "verifyMessage": "Verify the Hash and Website values before proceeding. If the values are correct, save the project.", - "saveBtn": "Save" + "saveBtn": "Save", + "sessionInactiveMessage": "You will be timed out in 60 secs due to inactivity. Please click anywhere to re-activate your session", + "sessionInactivityLogoutMessage": "You have been logged out due to inactivity." }, "breadcrumb": { "home": "Home", diff --git a/src/assets/i18n/fra.json b/src/assets/i18n/fra.json index e446fa50..65a8a26d 100644 --- a/src/assets/i18n/fra.json +++ b/src/assets/i18n/fra.json @@ -38,7 +38,9 @@ "projectName": "nom du projet", "addProject": "Ajouter un projet", "verifyMessage": "Vérifiez les valeurs de hachage et de site Web avant de continuer. Si les valeurs sont correctes, enregistrez le projet.", - "saveBtn": "Sauvegarder" + "saveBtn": "Sauvegarder", + "sessionInactiveMessage": "Votre temps sera écoulé dans 60 secondes en raison d'une inactivité. Veuillez cliquer n'importe où pour réactiver votre session", + "sessionInactivityLogoutMessage": "Vous avez été déconnecté pour cause d'inactivité." }, "breadcrumb": { "home": "Maison",