⚡ Fix duplicate refresh requests
This commit is contained in:
parent
0f85f951ab
commit
550b7ac328
@ -1,7 +1,7 @@
|
|||||||
import { HttpClient } from "@angular/common/http";
|
import { HttpClient } from "@angular/common/http";
|
||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { Observable, of } from "rxjs";
|
import { Observable, of, ReplaySubject } from "rxjs";
|
||||||
import { tap } from "rxjs/operators";
|
import { tap } from "rxjs/operators";
|
||||||
import { ApiService } from "./api.service";
|
import { ApiService } from "./api.service";
|
||||||
import { UtilsService } from "./utils.service";
|
import { UtilsService } from "./utils.service";
|
||||||
@ -22,7 +22,8 @@ const JWT_USER = "TRIP_USER";
|
|||||||
|
|
||||||
@Injectable({ providedIn: "root" })
|
@Injectable({ providedIn: "root" })
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
public apiBaseUrl: string;
|
public readonly apiBaseUrl: string;
|
||||||
|
private refreshInProgressLock$: ReplaySubject<Token> | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private httpClient: HttpClient,
|
private httpClient: HttpClient,
|
||||||
@ -73,15 +74,32 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshAccessToken(): Observable<Token> {
|
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", {
|
.post<Token>(this.apiBaseUrl + "/auth/refresh", {
|
||||||
refresh_token: this.refreshToken,
|
refresh_token: this.refreshToken,
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
tap((tokens: Token) => {
|
tap((tokens: Token) => {
|
||||||
this.accessToken = tokens.access_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> {
|
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 { 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 { AuthService } from "./auth.service";
|
||||||
import { UtilsService } from "./utils.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 authService = inject(AuthService);
|
||||||
const utilsService = inject(UtilsService);
|
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")) {
|
if (!req.headers.has("enctype") && !req.headers.has("Content-Type")) {
|
||||||
req = req.clone({
|
req = req.clone({
|
||||||
setHeaders: {
|
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)) {
|
if (req.url.startsWith(authService.apiBaseUrl)) {
|
||||||
req = req.clone({
|
req = req.clone({
|
||||||
setHeaders: { Authorization: `Bearer ${authService.accessToken}` },
|
setHeaders: { Authorization: `Bearer ${authService.accessToken}` },
|
||||||
@ -27,60 +62,13 @@ export const Interceptor = (req: HttpRequest<unknown>, next: HttpHandlerFn): Obs
|
|||||||
|
|
||||||
return next(req).pipe(
|
return next(req).pipe(
|
||||||
catchError((err: HttpErrorResponse) => {
|
catchError((err: HttpErrorResponse) => {
|
||||||
if (err.status == 400) {
|
const errDetails = ERROR_CONFIG[err.status];
|
||||||
|
if (errDetails) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
utilsService.toast(
|
return showAndThrowError(
|
||||||
"error",
|
errDetails.title,
|
||||||
"Bad Request",
|
`${err.error?.detail || err.error || errDetails.detail}`,
|
||||||
`${err.error?.detail || err.error || "Unknown error, check console for details"}`
|
|
||||||
);
|
);
|
||||||
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) {
|
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
|
// Unauthenticated, AT exists but is expired (authServices.accessToken truethy), we refresh it
|
||||||
return authService.refreshAccessToken().pipe(
|
return authService.refreshAccessToken().pipe(
|
||||||
|
take(1),
|
||||||
switchMap((tokens) => {
|
switchMap((tokens) => {
|
||||||
req = req.clone({
|
const refreshedReq = req.clone({
|
||||||
setHeaders: {
|
setHeaders: {
|
||||||
Authorization: `Bearer ${tokens.access_token}`,
|
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 any API route 401 -> redirect to login. We skip /refresh/ to prevent toast on login errors.
|
||||||
if (!req.url.endsWith("/refresh")) {
|
authService.logout(
|
||||||
if (err instanceof HttpErrorResponse && err.status === 401) {
|
`${err.error?.detail || err.error || "You must be authenticated"}`,
|
||||||
authService.logout(`${err.error?.detail || err.error || "You must be authenticated"}`, true);
|
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