diff --git a/src/app/core/authentication/authentication.service.ts b/src/app/core/authentication/authentication.service.ts index 38da8b646..417b49cfe 100644 --- a/src/app/core/authentication/authentication.service.ts +++ b/src/app/core/authentication/authentication.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http'; /** rxjs Imports */ -import { Observable, of } from 'rxjs'; +import { BehaviorSubject, Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; /** Custom Services */ @@ -30,6 +30,8 @@ export class AuthenticationService { } // User logged in boolean private userLoggedIn: boolean; + private userLoggedIn$: BehaviorSubject = new BehaviorSubject(false); + public readonly isAuthenticated$ = this.userLoggedIn$.asObservable(); /** Denotes whether the user credentials should persist through sessions. */ private rememberMe: boolean; @@ -84,6 +86,9 @@ export class AuthenticationService { if (twoFactorAccessToken) { authenticationInterceptor.setTwoFactorAccessToken(twoFactorAccessToken.token); } + // Emit the correct initial state + this.userLoggedIn = true; + this.userLoggedIn$.next(true); } } @@ -200,6 +205,7 @@ export class AuthenticationService { */ private onLoginSuccess(credentials: Credentials) { this.userLoggedIn = true; + this.userLoggedIn$.next(true); // ✅ notify observers // Ensure the rememberMe value is preserved in credentials credentials.rememberMe = this.rememberMe; @@ -268,6 +274,7 @@ export class AuthenticationService { this.setCredentials(); this.resetDialog(); this.userLoggedIn = false; + this.userLoggedIn$.next(false); // ✅ notify observers return of(true); } diff --git a/src/app/home/timeout-dialog/idle-timeout.service.ts b/src/app/home/timeout-dialog/idle-timeout.service.ts index 7b873ebc2..2fd3d17f4 100644 --- a/src/app/home/timeout-dialog/idle-timeout.service.ts +++ b/src/app/home/timeout-dialog/idle-timeout.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { environment } from '../../../environments/environment'; -import { interval, merge, fromEvent, Observable } from 'rxjs'; -import { takeUntil, repeat, map } from 'rxjs/operators'; +import { fromEvent, merge, Subject, timer, Observable, Subscription } from 'rxjs'; +import { switchMap, takeUntil, tap, map } from 'rxjs/operators'; /** * Idle timeout service used to track idle user @@ -11,24 +11,59 @@ import { takeUntil, repeat, map } from 'rxjs/operators'; }) export class IdleTimeoutService { // max timeout for an idle user - readonly timeoutDelay = environment.session.timeout.idleTimeout || 300000; // 5 minutes + private readonly timeoutDelay = environment.session.timeout.idleTimeout || 300000; + private timeout$ = new Subject(); + private resetTimer$ = new Subject(); + private active = false; + private timerSubscription?: Subscription; + private userActionsSubscription?: Subscription; // observable timeout readonly $onSessionTimeout: Observable; constructor() { - const events = [ - 'mousemove', - 'keydown', - 'wheel', - 'mousedown', - 'scroll' - ]; - const $signal = merge(...events.map((eventName) => fromEvent(document, eventName))); - this.$onSessionTimeout = interval(this.timeoutDelay).pipe( - takeUntil($signal), - map((): void => undefined), - repeat() - ); + this.$onSessionTimeout = this.timeout$.asObservable(); + + this.resetTimer$.subscribe(() => { + this.timerSubscription?.unsubscribe(); + this.timerSubscription = timer(this.timeoutDelay).subscribe(() => { + this.timeout$.next(); + this.stop(); + }); + }); + } + + start() { + if (!this.active) { + this.active = true; + this.reset(); + + // Subscribe to user actions only when active + const events = [ + 'mousemove', + 'keydown', + 'wheel', + 'mousedown', + 'scroll' + ]; + const userActions$ = merge(...events.map((e) => fromEvent(document, e))); + this.userActionsSubscription = userActions$.subscribe(() => { + this.reset(); + }); + } + } + + stop() { + if (this.active) { + this.active = false; + this.timerSubscription?.unsubscribe(); + this.userActionsSubscription?.unsubscribe(); + } + } + + reset() { + if (this.active) { + this.resetTimer$.next(); + } } } diff --git a/src/app/web-app.component.ts b/src/app/web-app.component.ts index cdaa9b6e9..c7e12ff46 100644 --- a/src/app/web-app.component.ts +++ b/src/app/web-app.component.ts @@ -1,12 +1,12 @@ /** Angular Imports */ -import { Component, OnInit, HostListener, HostBinding } from '@angular/core'; +import { Component, OnInit, HostListener, HostBinding, OnDestroy } from '@angular/core'; import { Router, NavigationEnd, ActivatedRoute } from '@angular/router'; import { Title } from '@angular/platform-browser'; import { MatSnackBar } from '@angular/material/snack-bar'; import { MatDialog } from '@angular/material/dialog'; /** rxjs Imports */ -import { merge } from 'rxjs'; +import { merge, Subscription } from 'rxjs'; import { filter, map, mergeMap } from 'rxjs/operators'; /** Translation Imports */ @@ -87,12 +87,12 @@ registerLocaleData(localeSW); // eslint-disable-next-line @angular-eslint/prefer-standalone standalone: false }) -export class WebAppComponent implements OnInit { +export class WebAppComponent implements OnInit, OnDestroy { buttonConfig: KeyboardShortcutsConfiguration; i18nService: I18nService; - isLoggedIn = false; + private authSubscription: Subscription; /** * @param {Router} router Router for navigation. @@ -231,6 +231,14 @@ export class WebAppComponent implements OnInit { // Subscribe to session timeout If IdleTimeout is higher than 0 (zero) if (environment.session.timeout.idleTimeout > 0) { + this.authSubscription = this.authenticationService.isAuthenticated$.subscribe((loggedIn) => { + if (loggedIn) { + this.idle.start(); + } else { + this.idle.stop(); + } + }); + this.idle.$onSessionTimeout.subscribe(() => { this.alertService.alert({ type: 'Session timeout', @@ -248,6 +256,12 @@ export class WebAppComponent implements OnInit { } } + ngOnDestroy() { + if (this.authSubscription) { + this.authSubscription.unsubscribe(); + } + } + logout() { this.authenticationService.logout().subscribe(() => this.router.navigate(['/login'], { replaceUrl: true })); }