Browse Source

Task/improve type safety in HTTP interceptor (#6957)

* feat(client): resolve errors

* feat(client): replace constructor based DI with inject function

* feat(client): enforce encapsulation and immutability

* feat(client): remove unused tap operator

* feat(client): implement generic type parameter

* feat(client): replace constructor based DI with inject functions
pull/4198/merge
Kenrick Tandrian 4 days ago
committed by GitHub
parent
commit
fea5ee33bb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      apps/client/src/app/core/auth.guard.ts
  2. 16
      apps/client/src/app/core/auth.interceptor.ts
  3. 45
      apps/client/src/app/core/http-response.interceptor.ts

12
apps/client/src/app/core/auth.guard.ts

@ -3,7 +3,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
import { internalRoutes, publicRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes, publicRoutes } from '@ghostfolio/common/routes/routes';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Router, Router,
@ -14,12 +14,10 @@ import { catchError } from 'rxjs/operators';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AuthGuard { export class AuthGuard {
public constructor( private readonly dataService = inject(DataService);
private dataService: DataService, private readonly router = inject(Router);
private router: Router, private readonly settingsStorageService = inject(SettingsStorageService);
private settingsStorageService: SettingsStorageService, private readonly userService = inject(UserService);
private userService: UserService
) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const utmSource = route.queryParams?.utm_source; const utmSource = route.queryParams?.utm_source;

16
apps/client/src/app/core/auth.interceptor.ts

@ -13,20 +13,20 @@ import {
HttpInterceptor, HttpInterceptor,
HttpRequest HttpRequest
} from '@angular/common/http'; } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@Injectable() @Injectable()
export class AuthInterceptor implements HttpInterceptor { export class AuthInterceptor implements HttpInterceptor {
public constructor( private readonly impersonationStorageService = inject(
private impersonationStorageService: ImpersonationStorageService, ImpersonationStorageService
private tokenStorageService: TokenStorageService );
) {} private readonly tokenStorageService = inject(TokenStorageService);
public intercept( public intercept<T>(
req: HttpRequest<any>, req: HttpRequest<T>,
next: HttpHandler next: HttpHandler
): Observable<HttpEvent<any>> { ): Observable<HttpEvent<T>> {
let request = req; let request = req;
if (request.headers.has(HEADER_KEY_SKIP_INTERCEPTOR)) { if (request.headers.has(HEADER_KEY_SKIP_INTERCEPTOR)) {

45
apps/client/src/app/core/http-response.interceptor.ts

@ -12,7 +12,7 @@ import {
HttpInterceptor, HttpInterceptor,
HttpRequest HttpRequest
} from '@angular/common/http'; } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { import {
MatSnackBar, MatSnackBar,
MatSnackBarRef, MatSnackBarRef,
@ -22,31 +22,28 @@ import { Router } from '@angular/router';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import ms from 'ms'; import ms from 'ms';
import { Observable, throwError } from 'rxjs'; import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@Injectable() @Injectable()
export class HttpResponseInterceptor implements HttpInterceptor { export class HttpResponseInterceptor implements HttpInterceptor {
public info: InfoItem; private readonly info: InfoItem;
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>; private snackBarRef: MatSnackBarRef<TextOnlySnackBar> | undefined;
public constructor( private readonly dataService = inject(DataService);
private dataService: DataService, private readonly router = inject(Router);
private router: Router, private readonly snackBar = inject(MatSnackBar);
private snackBar: MatSnackBar, private readonly userService = inject(UserService);
private userService: UserService, private readonly webAuthnService = inject(WebAuthnService);
private webAuthnService: WebAuthnService
) { public constructor() {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
} }
public intercept( public intercept<T>(
request: HttpRequest<any>, request: HttpRequest<T>,
next: HttpHandler next: HttpHandler
): Observable<HttpEvent<any>> { ): Observable<HttpEvent<T>> {
return next.handle(request).pipe( return next.handle(request).pipe(
tap((event: HttpEvent<any>) => {
return event;
}),
catchError((error: HttpErrorResponse) => { catchError((error: HttpErrorResponse) => {
if (error.status === StatusCodes.FORBIDDEN) { if (error.status === StatusCodes.FORBIDDEN) {
if (!this.snackBarRef) { if (!this.snackBarRef) {
@ -61,7 +58,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
} }
); );
} else if ( } else if (
!error.url.includes(internalRoutes.auth.routerLink.join('')) !error.url?.includes(internalRoutes.auth.routerLink.join(''))
) { ) {
this.snackBarRef = this.snackBar.open( this.snackBarRef = this.snackBar.open(
$localize`This action is not allowed.`, $localize`This action is not allowed.`,
@ -72,11 +69,11 @@ export class HttpResponseInterceptor implements HttpInterceptor {
); );
} }
this.snackBarRef.afterDismissed().subscribe(() => { this.snackBarRef?.afterDismissed().subscribe(() => {
this.snackBarRef = undefined; this.snackBarRef = undefined;
}); });
this.snackBarRef.onAction().subscribe(() => { this.snackBarRef?.onAction().subscribe(() => {
this.router.navigate(publicRoutes.pricing.routerLink); this.router.navigate(publicRoutes.pricing.routerLink);
}); });
} }
@ -92,11 +89,11 @@ export class HttpResponseInterceptor implements HttpInterceptor {
} }
); );
this.snackBarRef.afterDismissed().subscribe(() => { this.snackBarRef?.afterDismissed().subscribe(() => {
this.snackBarRef = undefined; this.snackBarRef = undefined;
}); });
this.snackBarRef.onAction().subscribe(() => { this.snackBarRef?.onAction().subscribe(() => {
window.location.reload(); window.location.reload();
}); });
} }
@ -106,12 +103,12 @@ export class HttpResponseInterceptor implements HttpInterceptor {
$localize`Oops! It looks like you’re making too many requests. Please slow down a bit.` $localize`Oops! It looks like you’re making too many requests. Please slow down a bit.`
); );
this.snackBarRef.afterDismissed().subscribe(() => { this.snackBarRef?.afterDismissed().subscribe(() => {
this.snackBarRef = undefined; this.snackBarRef = undefined;
}); });
} }
} else if (error.status === StatusCodes.UNAUTHORIZED) { } else if (error.status === StatusCodes.UNAUTHORIZED) {
if (!error.url.includes('/data-providers/ghostfolio/status')) { if (!error.url?.includes('/data-providers/ghostfolio/status')) {
if (this.webAuthnService.isEnabled()) { if (this.webAuthnService.isEnabled()) {
this.router.navigate(internalRoutes.webauthn.routerLink); this.router.navigate(internalRoutes.webauthn.routerLink);
} else { } else {

Loading…
Cancel
Save