From e308a7bace0b5836b456224c87d4b1b52ac4edc1 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Tue, 21 May 2024 21:21:33 +0200 Subject: [PATCH] added intercepter + auto login --- frontend/src/app/app.component.ts | 10 ++- frontend/src/app/app.config.ts | 3 + .../shared/interceptors/auth.interceptor.ts | 51 ++++++++++++++ frontend/src/app/shared/interceptors/index.ts | 0 .../src/app/shared/service/auth.service.ts | 67 +++++++++++++------ 5 files changed, 106 insertions(+), 25 deletions(-) create mode 100644 frontend/src/app/shared/interceptors/auth.interceptor.ts create mode 100644 frontend/src/app/shared/interceptors/index.ts diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 76f2faf..2ffb389 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,11 +1,15 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { RouterOutlet } from '@angular/router'; + +import { AuthService } from './shared/service'; @Component({ selector: 'app-root', standalone: true, - providers: [], + providers: [AuthService], imports: [RouterOutlet], templateUrl: './app.component.html', styleUrl: './app.component.scss', }) -export class AppComponent {} +export class AppComponent { + private readonly authService: AuthService = inject(AuthService); +} diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 200a054..9b95e78 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,11 +1,14 @@ +import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { ApplicationConfig } from '@angular/core'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideRouter, withComponentInputBinding } from '@angular/router'; import { routes } from './app.routes'; +import { AuthInterceptor } from './shared/interceptors/auth.interceptor'; export const appConfig: ApplicationConfig = { providers: [ + provideHttpClient(withInterceptors([AuthInterceptor])), provideRouter(routes, withComponentInputBinding()), provideAnimations(), ], diff --git a/frontend/src/app/shared/interceptors/auth.interceptor.ts b/frontend/src/app/shared/interceptors/auth.interceptor.ts new file mode 100644 index 0000000..7259b6c --- /dev/null +++ b/frontend/src/app/shared/interceptors/auth.interceptor.ts @@ -0,0 +1,51 @@ +import { + HttpErrorResponse, + HttpEvent, + HttpHandlerFn, + HttpInterceptorFn, + HttpRequest, +} from '@angular/common/http'; +import { inject } from '@angular/core'; + +import { Observable, catchError, switchMap, throwError } from 'rxjs'; + +import { AuthService } from '../service'; +import { Tokens } from '../types'; + +export const AuthInterceptor: HttpInterceptorFn = ( + request: HttpRequest, + next: HttpHandlerFn +): Observable> => { + const authService: AuthService = inject(AuthService); + const accessToken: string | null = authService.access_token; + + if (accessToken) { + request = request.clone({ + setHeaders: { + Authorization: `Bearer ${accessToken}`, + }, + }); + } + return next(request).pipe( + catchError((error: HttpErrorResponse) => { + if (error.status === 401) { + return authService.refreshToken().pipe( + switchMap((tokens: Tokens) => { + request = request.clone({ + setHeaders: { + Authorization: `Bearer ${tokens.access_token}`, + }, + }); + return next(request); + }), + catchError((refreshError) => { + authService.signout(); + return throwError(() => new Error(refreshError)); + }) + ); + } + + return throwError(() => new Error()); + }) + ); +}; diff --git a/frontend/src/app/shared/interceptors/index.ts b/frontend/src/app/shared/interceptors/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/shared/service/auth.service.ts b/frontend/src/app/shared/service/auth.service.ts index 4763c8b..a67ef5f 100644 --- a/frontend/src/app/shared/service/auth.service.ts +++ b/frontend/src/app/shared/service/auth.service.ts @@ -1,7 +1,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Observable, tap } from 'rxjs'; import { environment } from '../../../environments/environment'; import { LoginCredentials, Tokens } from '../types'; @@ -13,21 +13,27 @@ import { SessionStorageService } from './session-storage.service'; providedIn: 'root', }) export class AuthService { - private readonly path: string = '/api/auth'; - private access_token: string | null = null; - private refresh_token: string | null = null; - private isAuthenticated$: BehaviorSubject = + private readonly _path: string = '/api/auth'; + private _access_token: string | null = null; + private _refresh_token: string | null = null; + private _isAuthenticated$: BehaviorSubject = new BehaviorSubject(false); + public get access_token(): string | null { + return this._access_token; + } + public constructor( private readonly httpClient: HttpClient, private readonly localStorageService: LocalStorageService, private readonly sessionStorageService: SessionStorageService - ) {} + ) { + this.autoLogin(); + } public signin(credentials: LoginCredentials): void { this.httpClient - .post(environment.api.base + `${this.path}/signin`, credentials) + .post(environment.api.base + `${this._path}/signin`, credentials) .subscribe((response: Tokens) => { this.handleSuccess(response); }); @@ -35,43 +41,60 @@ export class AuthService { public signup(credentials: LoginCredentials): void { this.httpClient - .post(environment.api.base + `${this.path}/signup`, credentials) + .post(environment.api.base + `${this._path}/signup`, credentials) .subscribe((response: Tokens) => { - // The checked accept terms should be saved with a timestamp in the db + //TODO The checked accept terms should be saved with a timestamp in the db this.handleSuccess(response); }); } public signout(): void { - this.access_token = null; - this.refresh_token = null; + this._access_token = null; + this._refresh_token = null; this.localStorageService.removeItem('access_token'); this.sessionStorageService.removeItem('refresh_token'); - this.isAuthenticated$.next(false); + this._isAuthenticated$.next(false); } - public refreshToken(): void { + public autoLogin(): void { + const storedAccessToken: string | null = + this.localStorageService.getItem('access_token'); + const storedRefreshToken: string | null = + this.sessionStorageService.getItem('refresh_token'); + + if (storedAccessToken && storedRefreshToken) { + this._refresh_token = storedRefreshToken; + this._isAuthenticated$.next(true); + //TODO Validate tokens with backend or decode JWT to check expiration + } else { + this.signout(); + } + } + + public refreshToken(): Observable { const headers = new HttpHeaders().set( 'Authorization', - 'Bearer ' + this.refresh_token + 'Bearer ' + this._refresh_token ); - this.httpClient + return this.httpClient .post( - environment.api.base + `${this.path}/refresh`, + environment.api.base + `${this._path}/refresh`, {}, { headers: headers } ) - .subscribe((response: Tokens) => { - this.handleSuccess(response); - }); + .pipe( + tap((response: Tokens) => { + this.handleSuccess(response); + }) + ); } private handleSuccess(tokens: Tokens): void { - this.access_token = tokens.access_token; - this.refresh_token = tokens.refresh_token; + this._access_token = tokens.access_token; + this._refresh_token = tokens.refresh_token; this.localStorageService.setItem('access_token', tokens.access_token); this.sessionStorageService.setItem('refresh_token', tokens.refresh_token); - this.isAuthenticated$.next(true); + this._isAuthenticated$.next(true); } }