2024-07-02 00:54:21 +03:00
|
|
|
|
import {catchError, filter, mergeMap, Observable, retryWhen, switchMap, take, tap, timer} from "rxjs";
|
2024-06-05 21:43:36 +03:00
|
|
|
|
import {HttpClient, HttpErrorResponse} from "@angular/common/http";
|
|
|
|
|
import {NotifyColor, OpenNotifyService} from "@service/open-notify.service";
|
2024-06-15 00:37:12 +03:00
|
|
|
|
import {environment} from "@environment";
|
2024-06-05 21:43:36 +03:00
|
|
|
|
import {Router} from "@angular/router";
|
|
|
|
|
import {Injectable} from "@angular/core";
|
2024-06-27 01:50:58 +03:00
|
|
|
|
import {RequestBuilder, RequestData, SetRequestBuilderAfterBuild} from "@api/RequestBuilder";
|
2024-07-02 00:54:21 +03:00
|
|
|
|
import {TokenRefreshService} from "@service/token-refresh.service";
|
|
|
|
|
import {AuthToken} from "@service/auth.service";
|
2024-06-05 21:43:36 +03:00
|
|
|
|
|
|
|
|
|
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()
|
2024-06-27 01:50:58 +03:00
|
|
|
|
export default abstract class ApiService implements SetRequestBuilderAfterBuild {
|
2024-07-02 00:54:21 +03:00
|
|
|
|
constructor(private http: HttpClient, private notify: OpenNotifyService, private router: Router, protected tokenRefreshService: TokenRefreshService) {
|
2024-06-05 21:43:36 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-28 21:32:10 +03:00
|
|
|
|
private apiUrl = environment.apiUrl;
|
2024-06-05 21:43:36 +03:00
|
|
|
|
protected abstract basePath: string;
|
|
|
|
|
protected abstract version: AvailableVersion;
|
2024-06-27 01:50:58 +03:00
|
|
|
|
private request: RequestData = RequestBuilder.getStandardRequestData();
|
2024-07-02 00:54:21 +03:00
|
|
|
|
public static readonly tokenKey = 'auth_token';
|
2024-06-27 01:50:58 +03:00
|
|
|
|
|
|
|
|
|
public setRequestBuilder(request: RequestData): void {
|
|
|
|
|
this.request = request;
|
|
|
|
|
}
|
2024-06-05 21:43:36 +03:00
|
|
|
|
|
|
|
|
|
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) {
|
2024-06-27 01:50:58 +03:00
|
|
|
|
if (typeof (value) === typeof (Array)) {
|
2024-06-05 21:43:36 +03:00
|
|
|
|
(value as Array<any>).forEach(x => url.searchParams.append(key, x.toString()));
|
2024-06-27 01:50:58 +03:00
|
|
|
|
} else
|
2024-06-05 21:43:36 +03:00
|
|
|
|
url.searchParams.append(key, value.toString());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return url.href;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static combineUrls(...parts: string[]): string {
|
2024-06-28 21:32:10 +03:00
|
|
|
|
return parts.map(part => part.replace(/(^\/+|\/+$)/g, '')).join('/');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected get combinedUrl() {
|
2024-08-04 23:03:06 +03:00
|
|
|
|
return ApiService.addQuery(ApiService.combineUrls(this.apiUrl, AvailableVersion[this.version], this.basePath, this.request.endpoint), this.request.queryParams);
|
2024-06-05 21:43:36 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 01:50:58 +03:00
|
|
|
|
private makeHttpRequest<Type>(method: 'get' | 'post' | 'delete' | 'put'): Observable<Type> {
|
2024-06-28 21:32:10 +03:00
|
|
|
|
const doneEndpoint = this.combinedUrl;
|
|
|
|
|
|
|
|
|
|
return this.tokenRefreshService.getTokenRefreshing$().pipe(
|
2024-08-04 23:03:06 +03:00
|
|
|
|
filter(isRefreshing => !isRefreshing),
|
|
|
|
|
switchMap(() =>
|
|
|
|
|
this.http.request<Type>(method, doneEndpoint, {
|
2024-06-28 21:32:10 +03:00
|
|
|
|
withCredentials: this.request.withCredentials,
|
|
|
|
|
headers: this.request.httpHeaders,
|
|
|
|
|
body: this.request.data
|
|
|
|
|
}).pipe(
|
|
|
|
|
tap(_ => this.request = RequestBuilder.getStandardRequestData()),
|
|
|
|
|
retryWithInterval<Type>(),
|
|
|
|
|
catchError(error => {
|
|
|
|
|
if (!this.request.silenceMode)
|
|
|
|
|
this.handleError(error);
|
|
|
|
|
|
|
|
|
|
this.request = RequestBuilder.getStandardRequestData();
|
|
|
|
|
throw error;
|
|
|
|
|
})
|
2024-08-04 23:03:06 +03:00
|
|
|
|
)
|
|
|
|
|
)
|
2024-06-05 21:43:36 +03:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 01:50:58 +03:00
|
|
|
|
public createRequestBuilder() {
|
|
|
|
|
this.request = RequestBuilder.getStandardRequestData();
|
|
|
|
|
return new RequestBuilder(this);
|
2024-06-05 21:43:36 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 01:50:58 +03:00
|
|
|
|
public get<Type>(endpoint: string = ''): Observable<Type> {
|
|
|
|
|
if (endpoint)
|
|
|
|
|
this.request.endpoint = endpoint;
|
|
|
|
|
return this.makeHttpRequest<Type>('get');
|
2024-06-05 21:43:36 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 01:50:58 +03:00
|
|
|
|
public post<Type>(endpoint: string = ''): Observable<Type> {
|
|
|
|
|
if (endpoint)
|
|
|
|
|
this.request.endpoint = endpoint;
|
|
|
|
|
return this.makeHttpRequest<Type>('post');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public put<Type>(endpoint: string = ''): Observable<Type> {
|
|
|
|
|
if (endpoint)
|
|
|
|
|
this.request.endpoint = endpoint;
|
|
|
|
|
return this.makeHttpRequest<Type>('put');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public delete<Type>(endpoint: string = ''): Observable<Type> {
|
|
|
|
|
if (endpoint)
|
|
|
|
|
this.request.endpoint = endpoint;
|
|
|
|
|
return this.makeHttpRequest<Type>('delete');
|
2024-06-05 21:43:36 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-07-02 00:54:21 +03:00
|
|
|
|
public addAuth() {
|
|
|
|
|
const token = localStorage.getItem(ApiService.tokenKey);
|
|
|
|
|
|
|
|
|
|
if (!token)
|
|
|
|
|
return this;
|
|
|
|
|
|
|
|
|
|
const authToken = AuthToken.httpHeader((JSON.parse(token) as AuthToken));
|
2024-08-04 23:03:06 +03:00
|
|
|
|
authToken.keys().forEach(key => this.request.httpHeaders = this.request.httpHeaders.append(key, authToken.get(key) ?? ''));
|
2024-07-02 00:54:21 +03:00
|
|
|
|
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-05 21:43:36 +03:00
|
|
|
|
private handleError(error: HttpErrorResponse): void {
|
|
|
|
|
// todo: change to Retry-After condition
|
|
|
|
|
if (error.error.toString().includes("setup")) {
|
2024-06-27 01:50:58 +03:00
|
|
|
|
this.router.navigate(['/setup/']).then();
|
2024-06-05 21:43:36 +03:00
|
|
|
|
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:
|
2024-07-02 00:54:21 +03:00
|
|
|
|
this.router.navigate(['/login/']).then();
|
2024-06-05 21:43:36 +03:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|