WEB-384: Fix idle timer (#2733)

This commit is contained in:
Adam Saghy
2025-10-29 14:05:47 -06:00
committed by GitHub
parent 28c8f7b9c4
commit b63b0e19cc
3 changed files with 77 additions and 21 deletions
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http'; import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
/** rxjs Imports */ /** rxjs Imports */
import { Observable, of } from 'rxjs'; import { BehaviorSubject, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
/** Custom Services */ /** Custom Services */
@@ -30,6 +30,8 @@ export class AuthenticationService {
} }
// User logged in boolean // User logged in boolean
private userLoggedIn: boolean; private userLoggedIn: boolean;
private userLoggedIn$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public readonly isAuthenticated$ = this.userLoggedIn$.asObservable();
/** Denotes whether the user credentials should persist through sessions. */ /** Denotes whether the user credentials should persist through sessions. */
private rememberMe: boolean; private rememberMe: boolean;
@@ -84,6 +86,9 @@ export class AuthenticationService {
if (twoFactorAccessToken) { if (twoFactorAccessToken) {
authenticationInterceptor.setTwoFactorAccessToken(twoFactorAccessToken.token); 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) { private onLoginSuccess(credentials: Credentials) {
this.userLoggedIn = true; this.userLoggedIn = true;
this.userLoggedIn$.next(true); // ✅ notify observers
// Ensure the rememberMe value is preserved in credentials // Ensure the rememberMe value is preserved in credentials
credentials.rememberMe = this.rememberMe; credentials.rememberMe = this.rememberMe;
@@ -268,6 +274,7 @@ export class AuthenticationService {
this.setCredentials(); this.setCredentials();
this.resetDialog(); this.resetDialog();
this.userLoggedIn = false; this.userLoggedIn = false;
this.userLoggedIn$.next(false); // ✅ notify observers
return of(true); return of(true);
} }
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { interval, merge, fromEvent, Observable } from 'rxjs'; import { fromEvent, merge, Subject, timer, Observable, Subscription } from 'rxjs';
import { takeUntil, repeat, map } from 'rxjs/operators'; import { switchMap, takeUntil, tap, map } from 'rxjs/operators';
/** /**
* Idle timeout service used to track idle user * Idle timeout service used to track idle user
@@ -11,24 +11,59 @@ import { takeUntil, repeat, map } from 'rxjs/operators';
}) })
export class IdleTimeoutService { export class IdleTimeoutService {
// max timeout for an idle user // 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<void>();
private resetTimer$ = new Subject<void>();
private active = false;
private timerSubscription?: Subscription;
private userActionsSubscription?: Subscription;
// observable timeout // observable timeout
readonly $onSessionTimeout: Observable<void>; readonly $onSessionTimeout: Observable<void>;
constructor() { constructor() {
const events = [ this.$onSessionTimeout = this.timeout$.asObservable();
'mousemove',
'keydown', this.resetTimer$.subscribe(() => {
'wheel', this.timerSubscription?.unsubscribe();
'mousedown', this.timerSubscription = timer(this.timeoutDelay).subscribe(() => {
'scroll' this.timeout$.next();
]; this.stop();
const $signal = merge(...events.map((eventName) => fromEvent(document, eventName))); });
this.$onSessionTimeout = interval(this.timeoutDelay).pipe( });
takeUntil($signal), }
map((): void => undefined),
repeat() 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();
}
} }
} }
+18 -4
View File
@@ -1,12 +1,12 @@
/** Angular Imports */ /** 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 { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
/** rxjs Imports */ /** rxjs Imports */
import { merge } from 'rxjs'; import { merge, Subscription } from 'rxjs';
import { filter, map, mergeMap } from 'rxjs/operators'; import { filter, map, mergeMap } from 'rxjs/operators';
/** Translation Imports */ /** Translation Imports */
@@ -87,12 +87,12 @@ registerLocaleData(localeSW);
// eslint-disable-next-line @angular-eslint/prefer-standalone // eslint-disable-next-line @angular-eslint/prefer-standalone
standalone: false standalone: false
}) })
export class WebAppComponent implements OnInit { export class WebAppComponent implements OnInit, OnDestroy {
buttonConfig: KeyboardShortcutsConfiguration; buttonConfig: KeyboardShortcutsConfiguration;
i18nService: I18nService; i18nService: I18nService;
isLoggedIn = false; private authSubscription: Subscription;
/** /**
* @param {Router} router Router for navigation. * @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) // Subscribe to session timeout If IdleTimeout is higher than 0 (zero)
if (environment.session.timeout.idleTimeout > 0) { 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.idle.$onSessionTimeout.subscribe(() => {
this.alertService.alert({ this.alertService.alert({
type: 'Session timeout', type: 'Session timeout',
@@ -248,6 +256,12 @@ export class WebAppComponent implements OnInit {
} }
} }
ngOnDestroy() {
if (this.authSubscription) {
this.authSubscription.unsubscribe();
}
}
logout() { logout() {
this.authenticationService.logout().subscribe(() => this.router.navigate(['/login'], { replaceUrl: true })); this.authenticationService.logout().subscribe(() => this.router.navigate(['/login'], { replaceUrl: true }));
} }