Skip to content

Commit

Permalink
Bug fix for #77 (#91)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
limcaaarl and samuelim01 authored Nov 13, 2024
1 parent a3e960e commit de92e85
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 6 deletions.
48 changes: 46 additions & 2 deletions frontend/src/_services/authentication.service.ts
Original file line number Diff line number Diff line change
@@ -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<User | null>;
public user$: Observable<User | null>;

constructor(
private router: Router,
private http: HttpClient,
private toastService: ToastService,
) {
super();
const userData = localStorage.getItem('user');
Expand Down Expand Up @@ -53,6 +58,8 @@ export class AuthenticationService extends ApiService {
}
localStorage.setItem('user', JSON.stringify(user));
this.userSubject.next(user);
this.startTokenExpiryCheck();

return user;
}),
);
Expand Down Expand Up @@ -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;
}
}
1 change: 1 addition & 0 deletions frontend/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<main>
<app-navigation-bar />
<p-toast></p-toast>
<router-outlet />
</main>
16 changes: 12 additions & 4 deletions frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}

0 comments on commit de92e85

Please sign in to comment.