From c876a45ef8b1e8055f0ac41b8911fee716268d47 Mon Sep 17 00:00:00 2001 From: eneller Date: Thu, 23 Jul 2026 13:02:58 +0200 Subject: [PATCH] feat(client): auth interceptor --- client/src/app/app.config.ts | 3 +++ .../screens/screen-login/screen-login.html | 4 ++-- .../app/screens/screen-login/screen-login.ts | 6 ++++-- .../screens/screen-profile/screen-profile.ts | 2 +- client/src/app/services/api.ts | 15 +++++++++++---- .../src/app/services/auth-interceptor.spec.ts | 17 +++++++++++++++++ client/src/app/services/auth-interceptor.ts | 19 +++++++++++++++++++ 7 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 client/src/app/services/auth-interceptor.spec.ts create mode 100644 client/src/app/services/auth-interceptor.ts diff --git a/client/src/app/app.config.ts b/client/src/app/app.config.ts index fee4921..310ce60 100644 --- a/client/src/app/app.config.ts +++ b/client/src/app/app.config.ts @@ -3,6 +3,8 @@ import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { DATE_PIPE_DEFAULT_OPTIONS } from '@angular/common'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { authInterceptor } from './services/auth-interceptor'; export const appConfig: ApplicationConfig = { providers: [ @@ -10,5 +12,6 @@ export const appConfig: ApplicationConfig = { provideRouter(routes), {provide: DEFAULT_CURRENCY_CODE, useValue: ''}, {provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: {dateFormat: 'shortDate'}}, + provideHttpClient(withInterceptors([authInterceptor])), ] }; diff --git a/client/src/app/screens/screen-login/screen-login.html b/client/src/app/screens/screen-login/screen-login.html index c9462d1..2e75c76 100644 --- a/client/src/app/screens/screen-login/screen-login.html +++ b/client/src/app/screens/screen-login/screen-login.html @@ -57,15 +57,15 @@ + @if (error) { {{ error }} - + } diff --git a/client/src/app/screens/screen-login/screen-login.ts b/client/src/app/screens/screen-login/screen-login.ts index 5d408b5..604d8b4 100644 --- a/client/src/app/screens/screen-login/screen-login.ts +++ b/client/src/app/screens/screen-login/screen-login.ts @@ -4,6 +4,7 @@ import { Validators, FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, F import { ActivatedRoute, Router } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { APIService } from '../../services/api'; +import { GenericMessage } from '@message/Message'; @Component({ selector: 'app-screen-login', @@ -40,9 +41,10 @@ export class ScreenLogin { const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; this.router.navigateByUrl(returnUrl); }, - error: (err) => { + error: (resp) => { //FIXME error message displaying delayed, display message from server response - this.error = err.error?.message || 'Login failed. Please try again.'; + let msg: GenericMessage = resp.error; + this.error = msg.message || 'Login failed. Please try again.'; this.loading = false; } }); diff --git a/client/src/app/screens/screen-profile/screen-profile.ts b/client/src/app/screens/screen-profile/screen-profile.ts index f4db7e9..f16a718 100644 --- a/client/src/app/screens/screen-profile/screen-profile.ts +++ b/client/src/app/screens/screen-profile/screen-profile.ts @@ -36,7 +36,7 @@ export class ScreenProfile implements OnInit{ logOut(){ this.api.logout().subscribe({ next: () => { - this.router.navigate(['login']) + this.router.navigate(['/']) }, error: (err) => { console.error('Error logging out:', err) diff --git a/client/src/app/services/api.ts b/client/src/app/services/api.ts index eb80c66..b4ea88b 100644 --- a/client/src/app/services/api.ts +++ b/client/src/app/services/api.ts @@ -24,15 +24,22 @@ export class APIService { ); } logout(): Observable{ - return this.http.post(`${this.apiUrl}/auth/logout`, {}); + return this.http.post(`${this.apiUrl}/auth/logout`, {}).pipe( + tap({ + next: () => this.isAuthenticatedSubject.next(false), + error: () => this.isAuthenticatedSubject.next(false), + }) + ); + } + clearAuthState() { + this.isAuthenticatedSubject.next(false); } checkAuthStatus(): Observable { return this.http.get(`${this.apiUrl}/auth/status`).pipe( map(() => true), catchError(() => of(false)), - tap({ - next: () => this.isAuthenticatedSubject.next(true), - error: () => this.isAuthenticatedSubject.next(false), + tap( authenticated => { + this.isAuthenticatedSubject.next(authenticated); }) ); } diff --git a/client/src/app/services/auth-interceptor.spec.ts b/client/src/app/services/auth-interceptor.spec.ts new file mode 100644 index 0000000..5e54858 --- /dev/null +++ b/client/src/app/services/auth-interceptor.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpInterceptorFn } from '@angular/common/http'; + +import { authInterceptor } from './auth-interceptor'; + +describe('authInterceptor', () => { + const interceptor: HttpInterceptorFn = (req, next) => + TestBed.runInInjectionContext(() => authInterceptor(req, next)); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(interceptor).toBeTruthy(); + }); +}); diff --git a/client/src/app/services/auth-interceptor.ts b/client/src/app/services/auth-interceptor.ts new file mode 100644 index 0000000..f90d84c --- /dev/null +++ b/client/src/app/services/auth-interceptor.ts @@ -0,0 +1,19 @@ +import { HttpErrorResponse, HttpEventType, HttpInterceptorFn } from '@angular/common/http'; +import { catchError, throwError } from 'rxjs'; +import { APIService } from './api'; +import { inject } from '@angular/core'; +import { Router } from '@angular/router'; + +export const authInterceptor: HttpInterceptorFn = (req, next) => { + const api = inject(APIService); + const router = inject(Router) + return next(req).pipe( + catchError((err: HttpErrorResponse) => { + if (err.status === 401){ + api.clearAuthState(); + router.createUrlTree(['/login']); + } + return throwError(() => err); + }) + ) +};