MireaFrontend/src/api/api.service.ts

194 lines
7.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {catchError, distinctUntilChanged, filter, first, mergeMap, Observable, retryWhen, switchMap, timer} from "rxjs";
import {HttpClient, HttpErrorResponse} from "@angular/common/http";
import {NotifyColor, OpenNotifyService} from "@service/open-notify.service";
import {environment} from "@environment";
import {Router} from "@angular/router";
import {Injectable} from "@angular/core";
import {RequestBuilder, RequestData} from "@api/RequestBuilder";
import {TokenRefreshService} from "@service/token-refresh.service";
import {AuthToken} from "@service/auth.service";
export function retryWithInterval<T>(): (source: Observable<T>) => Observable<T> {
return (source: Observable<T>) =>
source.pipe(
retryWhen((errors: Observable<any>) =>
errors.pipe(
mergeMap((error, index) => {
if (index < (environment.maxRetry < 0 ? Infinity : environment.maxRetry - 1) && !error.status.toString().startsWith('4') && !error.status.toString().startsWith('5')) {
console.log(`Retrying after ${environment.retryDelay}ms...`);
return timer(environment.retryDelay);
} else {
if (error.status.toString().startsWith('4'))
console.error(`Server returned a client code error`);
else
console.error(`Exceeded maximum retries (${environment.maxRetry})`);
throw error;
}
})
)
)
);
}
export enum AvailableVersion {
v1
}
@Injectable()
export default abstract class ApiService {
constructor(private http: HttpClient, private notify: OpenNotifyService, private router: Router, protected tokenRefreshService: TokenRefreshService) {
}
private apiUrl = environment.apiUrl;
protected abstract basePath: string;
protected abstract version: AvailableVersion;
public static readonly tokenKey = 'auth_token';
private static addQuery(endpoint: string, queryParams?: Record<string, string | number | boolean | Array<any> | null> | null): string {
const url = new URL(endpoint);
if (queryParams) {
Object.keys(queryParams).forEach(key => {
const value = queryParams[key];
if (value !== null && value !== undefined) {
if (typeof (value) === typeof (Array)) {
(value as Array<any>).forEach(x => url.searchParams.append(key, x.toString()));
} else
url.searchParams.append(key, value.toString());
}
});
}
return url.href;
}
private static combineUrls(...parts: string[]): string {
return parts.map(part => part.replace(/(^\/+|\/+$)/g, '')).join('/');
}
protected combinedUrl(request: RequestData) {
return ApiService.addQuery(ApiService.combineUrls(this.apiUrl, AvailableVersion[this.version], this.basePath, request.endpoint), request.queryParams);
}
private sendHttpRequest<Type>(method: 'get' | 'post' | 'delete' | 'put', request: RequestData): Observable<Type> {
const doneEndpoint = this.combinedUrl(request);
return this.http.request<Type>(method, doneEndpoint, {
withCredentials: request.withCredentials,
headers: request.httpHeaders,
body: request.data
}).pipe(
retryWithInterval<Type>(),
catchError(error => {
if (!request.silenceMode)
this.handleError(error);
throw error;
})
);
}
private makeHttpRequest<Type>(method: 'get' | 'post' | 'delete' | 'put', request: RequestData): Observable<Type> {
if (request.needAuth)
return this.tokenRefreshService.getTokenRefreshing$().pipe(
distinctUntilChanged(),
filter(isRefreshing => !isRefreshing),
first(),
switchMap(() => {
const token = localStorage.getItem(ApiService.tokenKey);
if (token) {
const authToken = AuthToken.httpHeader((JSON.parse(token) as AuthToken));
authToken.keys().forEach(key => request.httpHeaders = request.httpHeaders.append(key, authToken.get(key) ?? ''));
}
return this.sendHttpRequest<Type>(method, request);
})
);
return this.sendHttpRequest<Type>(method, request);
}
private getRequest(request: RequestData | string | null): RequestData {
if (request === null)
return this.createRequestBuilder().build;
if (typeof request === 'string')
return this.createRequestBuilder().setEndpoint(request as string).build;
return request as RequestData;
}
public createRequestBuilder() {
return new RequestBuilder();
}
public get<Type>(request: RequestData | string | null = null): Observable<Type> {
return this.makeHttpRequest<Type>('get', this.getRequest(request));
}
public post<Type>(request: RequestData | string | null = null): Observable<Type> {
return this.makeHttpRequest<Type>('post', this.getRequest(request));
}
public put<Type>(request: RequestData | string | null = null): Observable<Type> {
return this.makeHttpRequest<Type>('put', this.getRequest(request));
}
public delete<Type>(request: RequestData | string | null = null): Observable<Type> {
return this.makeHttpRequest<Type>('delete', this.getRequest(request));
}
public addAuth(request: RequestData) {
request.needAuth = true;
return this;
}
private handleError(error: HttpErrorResponse): void {
// todo: change to Retry-After condition
if (error.error && error.error.toString().includes("setup")) {
this.router.navigate(['/setup/']).then();
return;
}
let message: string;
if (error.error instanceof ErrorEvent) {
message = `Произошла ошибка: ${error.error.message}`;
} else {
switch (error.status) {
case 0:
message = 'Неизвестная ошибка. Пожалуйста, попробуйте позже.';
break;
case 400:
message = 'Ошибка запроса. Пожалуйста, проверьте отправленные данные.';
break;
case 401:
this.router.navigate(['/login/']).then();
message = 'Ошибка авторизации. Пожалуйста, выполните вход с правильными учетными данными.';
break;
case 403:
message = 'Отказано в доступе. У вас нет разрешения на выполнение этого действия.';
break;
case 404:
message = 'Запрашиваемый ресурс не найден.';
break;
case 500:
message = 'Внутренняя ошибка сервера. Пожалуйста, попробуйте позже.';
break;
case 503:
message = 'Сервер на обслуживании. Пожалуйста, попробуйте позже.';
break;
default:
message = `Сервер вернул код ошибки: ${error.status}`;
break;
}
if (error.error?.Error) {
message += ` ${error.error.Error}`;
}
}
this.notify.open(message, NotifyColor.Danger);
}
}