⚡ Fix duplicate refresh requests
This commit is contained in:
parent
0f85f951ab
commit
550b7ac328
@ -1,7 +1,7 @@
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Observable, of } from "rxjs";
|
||||
import { Observable, of, ReplaySubject } from "rxjs";
|
||||
import { tap } from "rxjs/operators";
|
||||
import { ApiService } from "./api.service";
|
||||
import { UtilsService } from "./utils.service";
|
||||
@ -22,7 +22,8 @@ const JWT_USER = "TRIP_USER";
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class AuthService {
|
||||
public apiBaseUrl: string;
|
||||
public readonly apiBaseUrl: string;
|
||||
private refreshInProgressLock$: ReplaySubject<Token> | null = null;
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
@ -73,15 +74,32 @@ export class AuthService {
|
||||
}
|
||||
|
||||
refreshAccessToken(): Observable<Token> {
|
||||
return this.httpClient
|
||||
if (this.refreshInProgressLock$) {
|
||||
return this.refreshInProgressLock$.asObservable();
|
||||
}
|
||||
|
||||
this.refreshInProgressLock$ = new ReplaySubject(1);
|
||||
|
||||
this.httpClient
|
||||
.post<Token>(this.apiBaseUrl + "/auth/refresh", {
|
||||
refresh_token: this.refreshToken,
|
||||
})
|
||||
.pipe(
|
||||
tap((tokens: Token) => {
|
||||
this.accessToken = tokens.access_token;
|
||||
this.refreshInProgressLock$?.next(tokens);
|
||||
this.refreshInProgressLock$?.complete();
|
||||
this.refreshInProgressLock$ = null;
|
||||
}),
|
||||
);
|
||||
)
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
this.refreshInProgressLock$?.error(err);
|
||||
this.refreshInProgressLock$ = null;
|
||||
},
|
||||
});
|
||||
|
||||
return this.refreshInProgressLock$.asObservable();
|
||||
}
|
||||
|
||||
login(authForm: { username: string; password: string }): Observable<Token> {
|
||||
|
||||
@ -1,13 +1,45 @@
|
||||
import { HttpErrorResponse, HttpEvent, HttpHandlerFn, HttpRequest } from "@angular/common/http";
|
||||
import {
|
||||
HttpErrorResponse,
|
||||
HttpEvent,
|
||||
HttpHandlerFn,
|
||||
HttpRequest,
|
||||
} from "@angular/common/http";
|
||||
import { inject } from "@angular/core";
|
||||
import { catchError, Observable, switchMap, throwError } from "rxjs";
|
||||
import { catchError, Observable, switchMap, take, throwError } from "rxjs";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { UtilsService } from "./utils.service";
|
||||
|
||||
export const Interceptor = (req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> => {
|
||||
const ERROR_CONFIG: Record<number, { title: string; detail: string }> = {
|
||||
400: {
|
||||
title: "Bad Request",
|
||||
detail: "Unknown error, check console for details",
|
||||
},
|
||||
403: { title: "Forbidden", detail: "You are not allowed to do this" },
|
||||
409: { title: "Conflict", detail: "Conflict on resource" },
|
||||
413: { title: "Request Entity Too Large", detail: "The resource is too big" },
|
||||
422: {
|
||||
title: "Unprocessable Entity",
|
||||
detail: "The resource you sent was unprocessable",
|
||||
},
|
||||
502: {
|
||||
title: "Bad Gateway",
|
||||
detail: "Check your connectivity and ensure the server is up",
|
||||
},
|
||||
503: { title: "Service Unavailable", detail: "Resource not available" },
|
||||
};
|
||||
|
||||
export const Interceptor = (
|
||||
req: HttpRequest<unknown>,
|
||||
next: HttpHandlerFn,
|
||||
): Observable<HttpEvent<unknown>> => {
|
||||
const authService = inject(AuthService);
|
||||
const utilsService = inject(UtilsService);
|
||||
|
||||
function showAndThrowError(title: string, details: string) {
|
||||
utilsService.toast("error", title, details);
|
||||
return throwError(() => details);
|
||||
}
|
||||
|
||||
if (!req.headers.has("enctype") && !req.headers.has("Content-Type")) {
|
||||
req = req.clone({
|
||||
setHeaders: {
|
||||
@ -17,7 +49,10 @@ export const Interceptor = (req: HttpRequest<unknown>, next: HttpHandlerFn): Obs
|
||||
});
|
||||
}
|
||||
|
||||
if (authService.accessToken && !authService.isTokenExpired(authService.accessToken)) {
|
||||
if (
|
||||
authService.accessToken &&
|
||||
!authService.isTokenExpired(authService.accessToken)
|
||||
) {
|
||||
if (req.url.startsWith(authService.apiBaseUrl)) {
|
||||
req = req.clone({
|
||||
setHeaders: { Authorization: `Bearer ${authService.accessToken}` },
|
||||
@ -27,60 +62,13 @@ export const Interceptor = (req: HttpRequest<unknown>, next: HttpHandlerFn): Obs
|
||||
|
||||
return next(req).pipe(
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
if (err.status == 400) {
|
||||
const errDetails = ERROR_CONFIG[err.status];
|
||||
if (errDetails) {
|
||||
console.error(err);
|
||||
utilsService.toast(
|
||||
"error",
|
||||
"Bad Request",
|
||||
`${err.error?.detail || err.error || "Unknown error, check console for details"}`
|
||||
return showAndThrowError(
|
||||
errDetails.title,
|
||||
`${err.error?.detail || err.error || errDetails.detail}`,
|
||||
);
|
||||
return throwError(
|
||||
() => `Bad Request: ${err.error?.detail || err.error || "Unknown error, check console for details"}`
|
||||
);
|
||||
}
|
||||
|
||||
if (err.status == 403) {
|
||||
console.error(err);
|
||||
utilsService.toast("error", "Error", `${err.error?.detail || err.error || "You are not allowed to do this"}`);
|
||||
return throwError(() => `Bad Request: ${err.error?.detail || err.error || "You are not allowed to do this"}`);
|
||||
}
|
||||
|
||||
if (err.status == 409) {
|
||||
console.error(err);
|
||||
utilsService.toast("error", "Error", `${err.error?.detail || err.error || "Conflict on resource"}`);
|
||||
return throwError(() => `Bad Request: ${err.error?.detail || err.error || "Conflict on resource"}`);
|
||||
}
|
||||
|
||||
if (err.status == 413) {
|
||||
console.error(err);
|
||||
utilsService.toast(
|
||||
"error",
|
||||
"Request entity too large",
|
||||
"The resource you are trying to upload or create is too big"
|
||||
);
|
||||
return throwError(() => "Request entity too large, the resource you are trying to upload or create is too big");
|
||||
}
|
||||
|
||||
if (err.status == 422) {
|
||||
console.error(err);
|
||||
utilsService.toast("error", "Unprocessable Entity ", "The resource you sent was unprocessable");
|
||||
return throwError(() => "Resource sent was unprocessable");
|
||||
}
|
||||
|
||||
if (err.status == 502) {
|
||||
console.error(err);
|
||||
utilsService.toast("error", "Bad Gateway", "Check your connectivity and ensure the server is up and running");
|
||||
return throwError(() => "Bad Request: Check your connectivity and ensure the server is up and running");
|
||||
}
|
||||
|
||||
if (err.status == 503) {
|
||||
console.error(err);
|
||||
utilsService.toast(
|
||||
"error",
|
||||
"Service Unavailable",
|
||||
`${err.error?.detail || err.statusText || "Resource not available"}`
|
||||
);
|
||||
return throwError(() => "Service Unavailable: Resource not available");
|
||||
}
|
||||
|
||||
if (err.status == 401 && authService.accessToken) {
|
||||
@ -92,25 +80,29 @@ export const Interceptor = (req: HttpRequest<unknown>, next: HttpHandlerFn): Obs
|
||||
|
||||
// Unauthenticated, AT exists but is expired (authServices.accessToken truethy), we refresh it
|
||||
return authService.refreshAccessToken().pipe(
|
||||
take(1),
|
||||
switchMap((tokens) => {
|
||||
req = req.clone({
|
||||
const refreshedReq = req.clone({
|
||||
setHeaders: {
|
||||
Authorization: `Bearer ${tokens.access_token}`,
|
||||
},
|
||||
});
|
||||
return next(req);
|
||||
})
|
||||
return next(refreshedReq);
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
} else if (err.status == 401 && !req.url.endsWith("/refresh")) {
|
||||
// If any API route 401 -> redirect to login. We skip /refresh/ to prevent toast on login errors.
|
||||
if (!req.url.endsWith("/refresh")) {
|
||||
if (err instanceof HttpErrorResponse && err.status === 401) {
|
||||
authService.logout(`${err.error?.detail || err.error || "You must be authenticated"}`, true);
|
||||
}
|
||||
}
|
||||
authService.logout(
|
||||
`${err.error?.detail || err.error || "You must be authenticated"}`,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
return throwError(() => err);
|
||||
})
|
||||
console.error(err);
|
||||
return showAndThrowError(
|
||||
"Request Error",
|
||||
`${err.error?.detail || err.error || "Unknown error, check console for details"}`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user