From dcad05294c056e79bd1452eefa6bb491448cfdd7 Mon Sep 17 00:00:00 2001
From: Sebastian-Ion TINCU <Sebastian-Ion.TINCU@ext.ec.europa.eu>
Date: Thu, 6 Jun 2024 13:48:11 +0200
Subject: [PATCH] EDELIVERY-12752 UI enhancement  Users are warned before
 session expire

[PR] Prompt users when session is about to expire.
---
 smp-angular/src/app/app.module.ts             |  4 ++
 .../session-expiration-dialog.component.html  |  6 ++
 .../session-expiration-dialog.component.ts    | 26 ++++++++
 .../src/app/http/http-session-interceptor.ts  | 60 +++++++++----------
 4 files changed, 65 insertions(+), 31 deletions(-)
 create mode 100644 smp-angular/src/app/common/dialogs/session-expiration-dialog/session-expiration-dialog.component.html
 create mode 100644 smp-angular/src/app/common/dialogs/session-expiration-dialog/session-expiration-dialog.component.ts

diff --git a/smp-angular/src/app/app.module.ts b/smp-angular/src/app/app.module.ts
index a2a267343..84483a36c 100644
--- a/smp-angular/src/app/app.module.ts
+++ b/smp-angular/src/app/app.module.ts
@@ -149,6 +149,9 @@ import {
 } from "./tools/dns-tools/dns-query-panel/dns-query-panel.component";
 import {ResourceFilterOptionsService} from "./common/services/resource-filter-options.service";
 import {HttpSessionInterceptor} from "./http/http-session-interceptor";
+import {
+  SessionExpirationDialogComponent
+} from "./common/dialogs/session-expiration-dialog/session-expiration-dialog.component";
 
 
 @NgModule({
@@ -235,6 +238,7 @@ import {HttpSessionInterceptor} from "./http/http-session-interceptor";
     UserCertificatesComponent,
     UserProfileComponent,
     UserProfilePanelComponent,
+    SessionExpirationDialogComponent,
   ],
   imports: [
     BrowserAnimationsModule,
diff --git a/smp-angular/src/app/common/dialogs/session-expiration-dialog/session-expiration-dialog.component.html b/smp-angular/src/app/common/dialogs/session-expiration-dialog/session-expiration-dialog.component.html
new file mode 100644
index 000000000..b4b363b05
--- /dev/null
+++ b/smp-angular/src/app/common/dialogs/session-expiration-dialog/session-expiration-dialog.component.html
@@ -0,0 +1,6 @@
+<h2 mat-dialog-title>Extend session</h2>
+<mat-dialog-content>Your session is about to expire in <b>{{data.timeLeft}}</b> seconds!<br />Would you like to logout now or extend it for another <b>{{data.timeout}}</b> seconds?</mat-dialog-content>
+<mat-dialog-actions>
+  <button mat-button mat-dialog-close (click)="onLogoutClicked()" tabindex="-1">Logout</button>
+  <button mat-button mat-dialog-close (click)="onExtendSessionClicked()">Extend</button>
+</mat-dialog-actions>
diff --git a/smp-angular/src/app/common/dialogs/session-expiration-dialog/session-expiration-dialog.component.ts b/smp-angular/src/app/common/dialogs/session-expiration-dialog/session-expiration-dialog.component.ts
new file mode 100644
index 000000000..8d459600e
--- /dev/null
+++ b/smp-angular/src/app/common/dialogs/session-expiration-dialog/session-expiration-dialog.component.ts
@@ -0,0 +1,26 @@
+import {Component, Inject} from '@angular/core';
+import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
+import {SecurityService} from "../../../security/security.service";
+
+@Component({
+  templateUrl: './session-expiration-dialog.component.html',
+})
+export class SessionExpirationDialogComponent {
+
+  constructor(@Inject(MAT_DIALOG_DATA) public data: any,
+              public dialogRef: MatDialogRef<SessionExpirationDialogComponent>,
+              public securityService: SecurityService) {
+  }
+
+  public onExtendSessionClicked() {
+    // just make another simple call to the backend which cancels out the current inactivity
+    this.securityService.isAuthenticated(true);
+    this.dialogRef.close();
+  }
+
+  onLogoutClicked() {
+    this.securityService.logout();
+    this.dialogRef.close();
+  }
+}
+
diff --git a/smp-angular/src/app/http/http-session-interceptor.ts b/smp-angular/src/app/http/http-session-interceptor.ts
index 80e2d3339..f8bfcf469 100644
--- a/smp-angular/src/app/http/http-session-interceptor.ts
+++ b/smp-angular/src/app/http/http-session-interceptor.ts
@@ -1,51 +1,49 @@
 import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http";
-import {Observable, Subscription} from "rxjs";
-import {Injectable, OnDestroy, OnInit} from "@angular/core";
-import {SecurityEventService} from "../security/security-event.service";
+import {Observable} from "rxjs";
+import {Injectable} from "@angular/core";
 import {SecurityService} from "../security/security.service";
 import {AlertMessageService} from "../common/alert-message/alert-message.service";
-
+import {MatDialog} from "@angular/material/dialog";
+import {
+  SessionExpirationDialogComponent
+} from "../common/dialogs/session-expiration-dialog/session-expiration-dialog.component";
+
+/*
+ * An custom interceptor that handles session expiration before it happens.
+ *
+ * Users are prompted 60 seconds before their HTTP sessions are about to expire
+ * and asked whether they would like to logout or extend the session time again.
+ */
 @Injectable({
   providedIn: 'root'
 })
-export class HttpSessionInterceptor implements HttpInterceptor, OnInit, OnDestroy {
-
-  private securityEventService: SecurityEventService;
-
-  private securityService: SecurityService;
+export class HttpSessionInterceptor implements HttpInterceptor {
 
-  private alertMessageService: AlertMessageService;
-
-  private loginSubscription: Subscription;
+  private readonly TIME_BEFORE_EXPIRATION_IN_SECONDS = 60;
 
   private timerId: number;
 
-  private sessionExpiringSoon = false;
-
-  constructor(securityService: SecurityService,
-              securityEventService: SecurityEventService,
-              alertMessageService: AlertMessageService) {
-    this.securityService = securityService;
-    this.securityEventService = securityEventService;
-    this.alertMessageService = alertMessageService;
-  }
-
-  ngOnInit() {
-    this.loginSubscription = this.securityEventService.onLoginSuccessEvent().subscribe(() => this.sessionExpiringSoon = false);
-  }
-
-  ngOnDestroy() {
-    this.loginSubscription.unsubscribe();
+  constructor(public securityService: SecurityService,
+              public alertMessageService: AlertMessageService,
+              private dialog: MatDialog) {
   }
 
   public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
     clearTimeout(this.timerId);
     let user = this.securityService.getCurrentUser();
-    if (user && user.sessionMaxIntervalTimeoutInSeconds && user.sessionMaxIntervalTimeoutInSeconds > 60) {
-      let timeout = (user.sessionMaxIntervalTimeoutInSeconds - 60) * 1000;
-      this.timerId = setTimeout(() => this.alertMessageService.warning("Your current session is about to expire!"), timeout);
+    if (user && user.sessionMaxIntervalTimeoutInSeconds && user.sessionMaxIntervalTimeoutInSeconds > this.TIME_BEFORE_EXPIRATION_IN_SECONDS) {
+      let timeout = (user.sessionMaxIntervalTimeoutInSeconds - this.TIME_BEFORE_EXPIRATION_IN_SECONDS) * 1000;
+      this.timerId = setTimeout(() => this.sessionExpiringSoon(user.sessionMaxIntervalTimeoutInSeconds), timeout);
     }
     return next.handle(req);
   }
 
+  private sessionExpiringSoon(timeout) {
+    this.dialog.open(SessionExpirationDialogComponent, {
+      data: {
+        timeLeft: this.TIME_BEFORE_EXPIRATION_IN_SECONDS,
+        timeout
+      }
+    });
+  }
 }
-- 
GitLab