import {catchError, filter, mergeMap, Observable, retryWhen, switchMap, take, tap, 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, SetRequestBuilderAfterBuild} from "@api/RequestBuilder"; import {TokenRefreshService} from "@service/token-refresh.service"; import {AuthToken} from "@service/auth.service"; export function retryWithInterval(): (source: Observable) => Observable { return (source: Observable) => source.pipe( retryWhen((errors: Observable) => 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 implements SetRequestBuilderAfterBuild { 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; private request: RequestData = RequestBuilder.getStandardRequestData(); public static readonly tokenKey = 'auth_token'; public setRequestBuilder(request: RequestData): void { this.request = request; } private static addQuery(endpoint: string, queryParams?: Record | 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).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 get combinedUrl() { return ApiService.addQuery(ApiService.combineUrls(this.apiUrl, AvailableVersion[this.version], this.basePath, this.request.endpoint), this.request.queryParams) } private makeHttpRequest(method: 'get' | 'post' | 'delete' | 'put'): Observable { const doneEndpoint = this.combinedUrl; return this.tokenRefreshService.getTokenRefreshing$().pipe( filter(refreshing => !refreshing), take(1), switchMap(_ => { return this.http.request(method, doneEndpoint, { withCredentials: this.request.withCredentials, headers: this.request.httpHeaders, body: this.request.data }).pipe( tap(_ => this.request = RequestBuilder.getStandardRequestData()), retryWithInterval(), catchError(error => { if (!this.request.silenceMode) this.handleError(error); this.request = RequestBuilder.getStandardRequestData(); throw error; }) ); }) ); } public createRequestBuilder() { this.request = RequestBuilder.getStandardRequestData(); return new RequestBuilder(this); } public get(endpoint: string = ''): Observable { if (endpoint) this.request.endpoint = endpoint; return this.makeHttpRequest('get'); } public post(endpoint: string = ''): Observable { if (endpoint) this.request.endpoint = endpoint; return this.makeHttpRequest('post'); } public put(endpoint: string = ''): Observable { if (endpoint) this.request.endpoint = endpoint; return this.makeHttpRequest('put'); } public delete(endpoint: string = ''): Observable { if (endpoint) this.request.endpoint = endpoint; return this.makeHttpRequest('delete'); } public addAuth() { const token = localStorage.getItem(ApiService.tokenKey); if (!token) return this; const authToken = AuthToken.httpHeader((JSON.parse(token) as AuthToken)); authToken.keys().forEach(key => this.request.httpHeaders = this.request.httpHeaders.append(key, authToken.get(key) ?? '')) return this; } private handleError(error: HttpErrorResponse): void { // todo: change to Retry-After condition if (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); } }