From a57e3951caf53947adab1379751fe61e297f70a4 Mon Sep 17 00:00:00 2001 From: Marc Schmidt Date: Sun, 23 Jun 2024 20:31:35 +0200 Subject: [PATCH] fix: ensure token is not expired when calling APIs Refs: #95 --- src/client/src/app/app.config.ts | 4 +- .../src/app/services/auth.interceptor.ts | 26 ++++++++----- src/client/src/app/services/auth.service.ts | 37 ++++++++++++++++--- 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/client/src/app/app.config.ts b/src/client/src/app/app.config.ts index 2a9ec08..7032bef 100644 --- a/src/client/src/app/app.config.ts +++ b/src/client/src/app/app.config.ts @@ -64,8 +64,8 @@ export const appConfig: ApplicationConfig = { inject(ThemeService); const authService = inject(AuthService); - return () => { - authService.init(); + return async () => { + await authService.init(); }; }, }, diff --git a/src/client/src/app/services/auth.interceptor.ts b/src/client/src/app/services/auth.interceptor.ts index 06e231d..6a3a7ab 100644 --- a/src/client/src/app/services/auth.interceptor.ts +++ b/src/client/src/app/services/auth.interceptor.ts @@ -1,6 +1,6 @@ import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { from, Observable, switchMap } from 'rxjs'; import { AuthService } from './auth.service'; @@ -9,14 +9,22 @@ export class AuthInterceptor implements HttpInterceptor { private readonly _authService = inject(AuthService); public intercept(req: HttpRequest, next: HttpHandler): Observable> { - const token = this._authService.token()?.token; - if (token && !req.headers.has('Authorization')) { - req = req.clone({ - setHeaders: { - Authorization: `Bearer ${token}`, - }, - }); + if (req.url.includes('api/auth/token')) { + return next.handle(req); } - return next.handle(req); + + return from(this._authService.ensureTokenNotExpired()).pipe( + switchMap(() => { + const token = this._authService.token()?.token; + if (token && !req.headers.has('Authorization')) { + req = req.clone({ + setHeaders: { + Authorization: `Bearer ${token}`, + }, + }); + } + return next.handle(req); + }) + ); } } diff --git a/src/client/src/app/services/auth.service.ts b/src/client/src/app/services/auth.service.ts index c4d09af..8ef70c7 100644 --- a/src/client/src/app/services/auth.service.ts +++ b/src/client/src/app/services/auth.service.ts @@ -7,7 +7,9 @@ import { inject, signal, } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Router } from '@angular/router'; +import { fromEvent } from 'rxjs'; import { AuthTokenInfo, @@ -47,6 +49,14 @@ export class AuthService implements OnDestroy { this.clearTokenRefreshTimeout(); } }); + + fromEvent(document, 'visibilitychange') + .pipe(takeUntilDestroyed()) + .subscribe(() => { + if (!document.hidden) { + this.ensureTokenNotExpired(); + } + }); } public async init() { @@ -82,6 +92,15 @@ export class AuthService implements OnDestroy { this._token.set(null); } + public async ensureTokenNotExpired() { + const expiration = this._token()?.expiresAt; + if (!expiration || expiration.getTime() < Date.now() + 60 * 1000) { + await this.refreshToken(); + } else if (expiration) { + this.updateTokenRefreshTimeout(expiration); + } + } + public async signIn(loginToken: string): Promise { if (!environment.authenticationRequired) return 'success'; @@ -121,19 +140,25 @@ export class AuthService implements OnDestroy { } private updateTokenRefreshTimeout(expiration: Date) { + if (this._tokenRefreshTimeout) clearTimeout(this._tokenRefreshTimeout); this._tokenRefreshTimeout = setTimeout( () => { - const loginToken = getLoginToken(); - if (loginToken) { - this.signIn(loginToken); - } else { - this.signOut(); - } + this.refreshToken(); }, Math.max(10000, expiration.getTime() - Date.now() - 1000 * 60) ); } + private async refreshToken() { + console.log('Refreshing token'); + const loginToken = getLoginToken(); + if (loginToken) { + await this.signIn(loginToken); + } else { + await this.signOut(); + } + } + private clearTokenRefreshTimeout() { if (this._tokenRefreshTimeout) clearTimeout(this._tokenRefreshTimeout); this._tokenRefreshTimeout = undefined;