From 78845ce8b37c661bef6498872fbdfd3130ba694f Mon Sep 17 00:00:00 2001 From: Simon Briere Date: Fri, 7 Jan 2022 10:00:36 -0500 Subject: [PATCH] Added idle detection and automatic logout. --- Frontend/package-lock.json | 16 ++++ Frontend/package.json | 2 + Frontend/src/app/app.component.ts | 77 ++++++++++++++++++- Frontend/src/app/app.module.ts | 4 + .../information/information.component.html | 8 ++ .../information/information.component.scss | 0 .../information/information.component.ts | 18 +++++ .../app/services/authentication.service.ts | 28 ++++--- 8 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 Frontend/src/app/components/information/information/information.component.html create mode 100644 Frontend/src/app/components/information/information/information.component.scss create mode 100644 Frontend/src/app/components/information/information/information.component.ts diff --git a/Frontend/package-lock.json b/Frontend/package-lock.json index ec2e117..2ca40d3 100644 --- a/Frontend/package-lock.json +++ b/Frontend/package-lock.json @@ -3683,6 +3683,22 @@ "resolved": "https://registry.npmjs.org/@mattlewis92/dom-autoscroller/-/dom-autoscroller-2.4.2.tgz", "integrity": "sha512-YbrUWREPGEjE/FU6foXcAT1YbVwqD/jkYnY1dFb0o4AxtP3s4xKBthlELjndZih8uwsDWgQZx1eNskRNe2BgZQ==" }, + "@ng-idle/core": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@ng-idle/core/-/core-11.1.0.tgz", + "integrity": "sha512-/hf3LDFz3UCTe2H6r1bq6Kn6mo5B5mxaU5XVqcDfE4Vdlx9evTqBXyl0VpWbuzZbohVCfWq31mEjbxg9lbY4bw==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@ng-idle/keepalive": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@ng-idle/keepalive/-/keepalive-11.0.3.tgz", + "integrity": "sha512-etnPYnDua/uaFQebDHfC40iBb22KwPVkbt24/9IJBH9A4TGZa6zBrb+8DoRiRJm8I/WBuXdCDeaWzVHElfc9pg==", + "requires": { + "tslib": "^2.0.0" + } + }, "@ngtools/webpack": { "version": "12.2.10", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-12.2.10.tgz", diff --git a/Frontend/package.json b/Frontend/package.json index cb5f6bc..7f35fef 100644 --- a/Frontend/package.json +++ b/Frontend/package.json @@ -22,6 +22,8 @@ "@angular/platform-browser": "^12.2.11", "@angular/platform-browser-dynamic": "^12.2.11", "@angular/router": "^12.2.11", + "@ng-idle/core": "^11.1.0", + "@ng-idle/keepalive": "^11.0.3", "@schematics/angular": "^9.1.15", "angular-calendar": "^0.28.28", "angular-material": "^1.2.3", diff --git a/Frontend/src/app/app.component.ts b/Frontend/src/app/app.component.ts index cf96596..bb0735a 100644 --- a/Frontend/src/app/app.component.ts +++ b/Frontend/src/app/app.component.ts @@ -11,6 +11,10 @@ import {WebsocketService} from '@services/websocket.service'; import {Pages} from '@core/utils/pages'; import {SelectedSourceService} from '@services/selected-source.service'; import {SessionManagerService} from '@services/session-manager.service'; +import {DEFAULT_INTERRUPTSOURCES, Idle} from '@ng-idle/core'; +import {Keepalive} from '@ng-idle/keepalive'; +import {MatDialog, MatDialogRef} from '@angular/material/dialog'; +import {InformationComponent} from '@components/information/information/information.component'; @Component({ selector: 'app-root', @@ -21,6 +25,7 @@ export class AppComponent implements OnInit, OnDestroy { title = 'Portail Web'; loading: boolean; private subscription: Subscription; + private inactivityDialog: MatDialogRef = null; constructor(private cookieService: CookieService, private authService: AuthenticationService, @@ -28,7 +33,10 @@ export class AppComponent implements OnInit, OnDestroy { private webSocketService: WebsocketService, private selectedSourceService: SelectedSourceService, private router: Router, - private sessionManagerService: SessionManagerService) { + private sessionManagerService: SessionManagerService, + private idle: Idle, + private keepalive: Keepalive, + private dialog: MatDialog) { } ngOnInit(): void { @@ -37,6 +45,73 @@ export class AppComponent implements OnInit, OnDestroy { this.navigationInterceptor(e); } }); + this.initIdleTimeout(); + } + + private initIdleTimeout(): void { + // sets an idle timeout of 2 hours + this.idle.setIdle(60 * 60 * 2); + // sets a timeout period of 60 seconds. After that delay, if no user input, it will be logged out. + this.idle.setTimeout(60); + // sets the default interrupts, in this case, things like clicks, scrolls, touches to the document + this.idle.setInterrupts(DEFAULT_INTERRUPTSOURCES); + + this.idle.onIdleEnd.subscribe(() => { + // console.log('No longer idle'); + this.resetIdleTimer(); + }); + + this.idle.onTimeout.subscribe(() => { + // console.log('Timed out!'); + this.inactivityDialog.close(); + this.authService.logout().subscribe(); + }); + + this.idle.onIdleStart.subscribe(() => { + // console.log('You\'ve gone idle!'); + this.idle.setInterrupts([]); + if (this.inactivityDialog == null){ + this.inactivityDialog = this.dialog.open(InformationComponent, { + width: '350px', + data: {message: 'Idle', button_text: 'Ne pas me déconnecter'} + }); + this.inactivityDialog.afterClosed().subscribe(() => { + // console.log('Dialog deleted'); + this.resetIdleTimer(); + this.inactivityDialog = null; + }); + } + }); + + this.idle.onTimeoutWarning.subscribe((countdown) => { + const idleState = 'Vous serez automatiquement déconnectés dans ' + countdown + ' secondes...'; + this.inactivityDialog.componentInstance.data.message = idleState; + // console.log(idleState); + }); + + // sets the ping interval to 15 seconds + this.keepalive.interval(15); + + // this.keepalive.onPing.subscribe(() => this.lastPing = new Date()); + + // this.resetIdleTimer(); + this.authService.loginStateChange().subscribe(userLoggedIn => { + if (userLoggedIn) { + this.resetIdleTimer(); + } else { + this.stopIdleTimer(); + } + }); + } + + private resetIdleTimer(): void{ + this.idle.stop(); + this.idle.setInterrupts(DEFAULT_INTERRUPTSOURCES); + this.idle.watch(); + } + + private stopIdleTimer(): void{ + this.idle.stop(); } private refreshToken(): void { diff --git a/Frontend/src/app/app.module.ts b/Frontend/src/app/app.module.ts index 001640b..c247ae0 100644 --- a/Frontend/src/app/app.module.ts +++ b/Frontend/src/app/app.module.ts @@ -15,6 +15,8 @@ import {ParticipantModule} from '@src/app/modules/participant.module'; import {HeaderModule} from '@src/app/modules/header.module'; import {EventDialogComponent} from '@components/event-dialog/event-dialog.component'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {NgIdleKeepaliveModule} from '@ng-idle/keepalive'; +import { InformationComponent } from './components/information/information/information.component'; registerLocaleData(localeFr, 'fr'); @@ -27,6 +29,7 @@ registerLocaleData(localeFr, 'fr'); ParticipantLayoutComponent, NotFoundComponent, EventDialogComponent, + InformationComponent, ], imports: [ SharedModule, @@ -36,6 +39,7 @@ registerLocaleData(localeFr, 'fr'); UserModule, ParticipantModule, AppRoutingModule, + NgIdleKeepaliveModule.forRoot(), ], providers: [], exports: [ diff --git a/Frontend/src/app/components/information/information/information.component.html b/Frontend/src/app/components/information/information/information.component.html new file mode 100644 index 0000000..4030316 --- /dev/null +++ b/Frontend/src/app/components/information/information/information.component.html @@ -0,0 +1,8 @@ +
+
+ {{data.message}} +
+
+ +
+
diff --git a/Frontend/src/app/components/information/information/information.component.scss b/Frontend/src/app/components/information/information/information.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/Frontend/src/app/components/information/information/information.component.ts b/Frontend/src/app/components/information/information/information.component.ts new file mode 100644 index 0000000..bb3ddad --- /dev/null +++ b/Frontend/src/app/components/information/information/information.component.ts @@ -0,0 +1,18 @@ +import {Component, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; + +@Component({ + selector: 'app-information', + templateUrl: './information.component.html', + styleUrls: ['./information.component.scss'] +}) +export class InformationComponent implements OnInit { + + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: {message: string, button_text: string}) { + } + + ngOnInit(): void { + } + +} diff --git a/Frontend/src/app/services/authentication.service.ts b/Frontend/src/app/services/authentication.service.ts index 8f7d8a7..25a0b65 100644 --- a/Frontend/src/app/services/authentication.service.ts +++ b/Frontend/src/app/services/authentication.service.ts @@ -1,5 +1,5 @@ import {Injectable} from '@angular/core'; -import {Observable} from 'rxjs'; +import {BehaviorSubject, Observable} from 'rxjs'; import {map, switchMap, tap} from 'rxjs/operators'; import {HttpClient, HttpHeaders} from '@angular/common/http'; import {GlobalConstants} from '@core/utils/global-constants'; @@ -23,11 +23,11 @@ export class AuthenticationService { private cookieValue = GlobalConstants.cookieValue; private API_URL = makeApiURL(); private refreshTokenTimeout: any; - private isLoggedIn = false; - - private _lastAuthenticatedPath: string = this.defaultPath; + // private isLoggedIn = false; + private isLoggedIn: BehaviorSubject = new BehaviorSubject(false); + private m_lastAuthenticatedPath: string = this.defaultPath; set lastAuthenticatedPath(value: string) { - this._lastAuthenticatedPath = value; + this.m_lastAuthenticatedPath = value; } constructor(private http: HttpClient, @@ -41,8 +41,12 @@ export class AuthenticationService { } isAuthenticated(): boolean { - this.isLoggedIn = !!this.cookieService.get(this.cookieValue); - return this.isLoggedIn; + this.isLoggedIn.next(!!this.cookieService.get(this.cookieValue)); + return this.isLoggedIn.getValue(); + } + + loginStateChange(): Observable { + return this.isLoggedIn.asObservable(); } login(username: string, password: string, isManager: boolean = false): Observable { @@ -51,9 +55,9 @@ export class AuthenticationService { return this.http.get(apiUrl, {headers}).pipe( tap((response: any) => { const token = isManager ? response.user_token : response.participant_token; - this.isLoggedIn = true; + this.isLoggedIn.next(true); this.cookieService.set(this.cookieValue, token, 0.5, '/'); - this.router.navigate([this._lastAuthenticatedPath]); + this.router.navigate([this.m_lastAuthenticatedPath]); // Connect websocket this.websocketService.connect(response.websocket_url); this.startRefreshTokenTimer(); @@ -62,9 +66,9 @@ export class AuthenticationService { } loginWithToken(token: string): void { - this.isLoggedIn = true; + this.isLoggedIn.next(true); this.cookieService.set(this.cookieValue, token, null, '/'); - this.router.navigate([this._lastAuthenticatedPath]); + this.router.navigate([this.m_lastAuthenticatedPath]); } logout(isManager: boolean = false): Observable { @@ -77,7 +81,7 @@ export class AuthenticationService { } reset(): void { - this.isLoggedIn = false; + this.isLoggedIn.next(false); this.router.navigate([Pages.loginPage]); this.cookieService.delete(this.cookieValue, '/'); this.selectedProjectService.setSelectedProject(new Project());