From cf4a994ad0c71d81e366f40b62362706810af9d1 Mon Sep 17 00:00:00 2001 From: Chneemann Date: Sun, 6 Apr 2025 11:01:59 +0200 Subject: [PATCH] feat: create AuthInterceptor and integrate it; adjust AuthGuards accordingly --- src/app/app.config.ts | 6 ++ .../main-layout/main-layout.component.ts | 1 - src/app/guards/authenticated.guard.ts | 39 ++++------- .../guards/redirect-if-authenticated.guard.ts | 19 +++--- src/app/interceptors/auth-interceptor.ts | 64 +++++++++++++++++++ .../services/toast-notification.service.ts | 2 +- src/app/services/token.service.ts | 15 ++--- 7 files changed, 98 insertions(+), 48 deletions(-) create mode 100644 src/app/interceptors/auth-interceptor.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index b4c8ce0..c16c85f 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -19,6 +19,7 @@ import { ToastrModule } from 'ngx-toastr'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpErrorInterceptor } from './interceptors/http-interceptor'; import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { AuthInterceptor } from './interceptors/auth-interceptor'; export function createTranslateLoader(http: HttpClient) { return new TranslateHttpLoader(http, './assets/i18n/', '.json'); @@ -68,5 +69,10 @@ export const appConfig: ApplicationConfig = { useClass: HttpErrorInterceptor, multi: true, }, + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true, + }, ], }; diff --git a/src/app/components/main-layout/main-layout.component.ts b/src/app/components/main-layout/main-layout.component.ts index 593b3f9..8ff8ce5 100644 --- a/src/app/components/main-layout/main-layout.component.ts +++ b/src/app/components/main-layout/main-layout.component.ts @@ -1,7 +1,6 @@ import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { CommonModule } from '@angular/common'; - import { LanguageService } from '../../services/language.service'; import { SidebarMobileComponent } from '../../shared/components/sidebar/sidebar-mobile/sidebar-mobile.component'; import { SidebarComponent } from '../../shared/components/sidebar/sidebar.component'; diff --git a/src/app/guards/authenticated.guard.ts b/src/app/guards/authenticated.guard.ts index 6dfb7df..7c89bd8 100644 --- a/src/app/guards/authenticated.guard.ts +++ b/src/app/guards/authenticated.guard.ts @@ -1,46 +1,33 @@ import { Injectable } from '@angular/core'; -import { Router } from '@angular/router'; -import { catchError, map, Observable, of } from 'rxjs'; +import { CanActivate, Router } from '@angular/router'; +import { map, Observable } from 'rxjs'; import { AuthService } from '../services/auth.service'; import { TokenService } from '../services/token.service'; -import { ToastrService } from 'ngx-toastr'; import { ToastNotificationService } from '../services/toast-notification.service'; @Injectable({ providedIn: 'root', }) -export class AuthenticatedGuard { +export class AuthenticatedGuard implements CanActivate { constructor( private authService: AuthService, private tokenService: TokenService, - private router: Router, - private toastrService: ToastrService, - private toastNotificationService: ToastNotificationService + private toastNotificationService: ToastNotificationService, + private router: Router ) {} canActivate(): Observable | Promise | boolean { - const authToken = this.tokenService.getAuthToken(); - - if (!authToken || !this.tokenService.isTokenExpired(authToken)) { + if ( + !this.tokenService.isTokenAvailable() || + !this.tokenService.isUserIdAvailable() + ) { this.router.navigate(['/login']); + this.toastNotificationService.showSessionExpiredToast(); return false; } - return this.authService.checkAuthUser().pipe( - map((isAuthenticated) => { - if (isAuthenticated) { - return true; - } else { - this.toastNotificationService.showSessionExpiredMessage(); - this.router.navigate(['/login']); - return false; - } - }), - catchError(() => { - this.toastNotificationService.showSessionExpiredMessage(); - this.router.navigate(['/login']); - return of(false); - }) - ); + return this.authService + .checkAuthUser() + .pipe(map((isAuthenticated) => isAuthenticated)); } } diff --git a/src/app/guards/redirect-if-authenticated.guard.ts b/src/app/guards/redirect-if-authenticated.guard.ts index 47e8725..7a703d9 100644 --- a/src/app/guards/redirect-if-authenticated.guard.ts +++ b/src/app/guards/redirect-if-authenticated.guard.ts @@ -1,14 +1,13 @@ import { Injectable } from '@angular/core'; -import { Router } from '@angular/router'; -import { Observable, of } from 'rxjs'; -import { map, catchError } from 'rxjs/operators'; +import { CanActivate, Router } from '@angular/router'; +import { Observable, map } from 'rxjs'; import { AuthService } from '../services/auth.service'; import { TokenService } from '../services/token.service'; @Injectable({ providedIn: 'root', }) -export class RedirectIfAuthenticatedGuard { +export class RedirectIfAuthenticatedGuard implements CanActivate { constructor( private authService: AuthService, private tokenService: TokenService, @@ -16,22 +15,20 @@ export class RedirectIfAuthenticatedGuard { ) {} canActivate(): Observable | Promise | boolean { - const authToken = this.tokenService.getAuthToken(); - - if (!authToken || !this.tokenService.isTokenExpired(authToken)) { + if ( + !this.tokenService.isTokenAvailable() || + !this.tokenService.isUserIdAvailable() + ) { return true; } return this.authService.checkAuthUser().pipe( map((isAuthenticated) => { if (isAuthenticated) { + this.router.navigate(['/summary']); return false; } return true; - }), - catchError(() => { - this.router.navigate(['/login']); - return of(false); }) ); } diff --git a/src/app/interceptors/auth-interceptor.ts b/src/app/interceptors/auth-interceptor.ts new file mode 100644 index 0000000..d269802 --- /dev/null +++ b/src/app/interceptors/auth-interceptor.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@angular/core'; +import { + HttpInterceptor, + HttpRequest, + HttpHandler, + HttpEvent, + HttpErrorResponse, +} from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; +import { catchError, filter } from 'rxjs/operators'; +import { Router, NavigationStart } from '@angular/router'; +import { TokenService } from '../services/token.service'; +import { ToastNotificationService } from '../services/toast-notification.service'; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + private isNavigatingToLogin = false; + + constructor( + private tokenService: TokenService, + private router: Router, + private toast: ToastNotificationService + ) { + this.router.events + .pipe( + filter( + (event): event is NavigationStart => event instanceof NavigationStart + ) + ) + .subscribe((event) => { + if (event.url === '/login') { + this.isNavigatingToLogin = true; + } else { + this.isNavigatingToLogin = false; + } + }); + } + + intercept( + req: HttpRequest, + next: HttpHandler + ): Observable> { + const token = this.tokenService.getAuthToken(); + let clonedReq = req; + + if (token) { + clonedReq = req.clone({ + setHeaders: { + Authorization: `Token ${token}`, + }, + }); + } + + return next.handle(clonedReq).pipe( + catchError((error: HttpErrorResponse) => { + if (error.status === 401 && !this.isNavigatingToLogin) { + this.toast.showSessionExpiredToast(); + this.router.navigate(['/login']); + } + return throwError(() => error); + }) + ); + } +} diff --git a/src/app/services/toast-notification.service.ts b/src/app/services/toast-notification.service.ts index 87b9ea1..62aa02d 100755 --- a/src/app/services/toast-notification.service.ts +++ b/src/app/services/toast-notification.service.ts @@ -43,7 +43,7 @@ export class ToastNotificationService { ); } - showSessionExpiredMessage(): void { + showSessionExpiredToast(): void { this.createInfoToast( 'Your session has expired, please log in again.', 'Session Expired' diff --git a/src/app/services/token.service.ts b/src/app/services/token.service.ts index e4b8e35..3b0dbc4 100755 --- a/src/app/services/token.service.ts +++ b/src/app/services/token.service.ts @@ -43,14 +43,11 @@ export class TokenService { sessionStorage.removeItem(this.USER_ID_KEY); } - isTokenExpired(token: string): boolean { - try { - const payload = JSON.parse(atob(token.split('.')[1])); - const expiry = payload.exp; - if (!expiry) return true; - return expiry * 1000 < Date.now(); - } catch (e) { - return true; - } + isTokenAvailable(): boolean { + return !!this.getAuthToken(); + } + + isUserIdAvailable(): boolean { + return !!this.getUserId(); } }