From de92e85c0f04bf698119694abaf7e10d07d92e24 Mon Sep 17 00:00:00 2001 From: limcaaarl <42115432+limcaaarl@users.noreply.github.com> Date: Wed, 13 Nov 2024 22:34:32 +0800 Subject: [PATCH] Bug fix for #77 (#91) * Bug fix for #77 - Users will now be warned if their tokens are about to expire - Users will be logged out upon token expiration * Update from toast to alert for expired token * Replace interval with timer * Enhance warning * Fix linting * Fix takeUntilDestroyed https://stackoverflow.com/questions/76264067/takeuntildestroyed-can-only-be-used-within-an-injection-context --------- Co-authored-by: Samuel Lim --- .../src/_services/authentication.service.ts | 48 ++++++++++++++++++- frontend/src/app/app.component.html | 1 + frontend/src/app/app.component.ts | 16 +++++-- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/frontend/src/_services/authentication.service.ts b/frontend/src/_services/authentication.service.ts index 3f450877f8..8458fac1c7 100644 --- a/frontend/src/_services/authentication.service.ts +++ b/frontend/src/_services/authentication.service.ts @@ -1,23 +1,28 @@ // Modified from https://jasonwatmore.com/post/2022/11/15/angular-14-jwt-authentication-example-tutorial#login-component-ts -import { Injectable } from '@angular/core'; +import { DestroyRef, inject, Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { HttpClient } from '@angular/common/http'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, Observable, timer } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { map, switchMap } from 'rxjs/operators'; import { UServRes } from '../_models/user.service.model'; import { User } from '../_models/user.model'; import { ApiService } from './api.service'; +import { ToastService } from './toast.service'; @Injectable({ providedIn: 'root' }) export class AuthenticationService extends ApiService { protected apiPath = 'user'; + private destroyRef = inject(DestroyRef); + private userSubject: BehaviorSubject; public user$: Observable; constructor( private router: Router, private http: HttpClient, + private toastService: ToastService, ) { super(); const userData = localStorage.getItem('user'); @@ -53,6 +58,8 @@ export class AuthenticationService extends ApiService { } localStorage.setItem('user', JSON.stringify(user)); this.userSubject.next(user); + this.startTokenExpiryCheck(); + return user; }), ); @@ -119,4 +126,41 @@ export class AuthenticationService extends ApiService { }), ); } + + displaySessionExpiryWarning(): void { + this.toastService.showToast('Your session will expire in less than 5 minutes. Please log in again.'); + } + + public startTokenExpiryCheck(): void { + const tokenExpirationTime = this.getTokenExpiration(); + if (!tokenExpirationTime) { + this.logout(); + return; + } + + const oneMinute = 60 * 1000; + const timeLeft = tokenExpirationTime - Date.now(); + if (timeLeft > 5 * oneMinute) { + timer(timeLeft - 5 * oneMinute) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => this.displaySessionExpiryWarning()); + } else { + this.displaySessionExpiryWarning(); + } + + timer(timeLeft) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + alert('Your session has expired. Please log in again.'); + this.logout(); + }); + } + + private getTokenExpiration() { + const user = this.userValue; + if (!user || !user.accessToken) return null; + + const tokenPayload = JSON.parse(atob(user.accessToken.split('.')[1])); + return tokenPayload.exp ? tokenPayload.exp * 1000 : null; + } } diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index a20a5cb8af..d702e4580e 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,4 +1,5 @@
+
diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index d0722c2b03..0e29dabf67 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,16 +1,24 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { ButtonModule } from 'primeng/button'; import { PasswordModule } from 'primeng/password'; +import { ToastModule } from 'primeng/toast'; import { NavigationBarComponent } from './navigation-bar/navigation-bar.component'; - +import { AuthenticationService } from '../_services/authentication.service'; +import { MessageService } from 'primeng/api'; @Component({ selector: 'app-root', standalone: true, - imports: [NavigationBarComponent, RouterOutlet, ButtonModule, PasswordModule], + imports: [NavigationBarComponent, RouterOutlet, ButtonModule, PasswordModule, ToastModule], + providers: [MessageService], templateUrl: './app.component.html', styleUrl: './app.component.css', }) -export class AppComponent { +export class AppComponent implements OnInit { title = 'frontend'; + + constructor(private authService: AuthenticationService) {} + ngOnInit() { + this.authService.startTokenExpiryCheck(); + } }