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(): (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 { 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 | 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 combinedUrl(request: RequestData) { return ApiService.addQuery(ApiService.combineUrls(this.apiUrl, AvailableVersion[this.version], this.basePath, request.endpoint), request.queryParams); } private sendHttpRequest(method: 'get' | 'post' | 'delete' | 'put', request: RequestData): Observable { const doneEndpoint = this.combinedUrl(request); return this.http.request(method, doneEndpoint, { withCredentials: request.withCredentials, headers: request.httpHeaders, body: request.data }).pipe( retryWithInterval(), catchError(error => { if (!request.silenceMode) this.handleError(error); throw error; }) ); } private makeHttpRequest(method: 'get' | 'post' | 'delete' | 'put', request: RequestData): Observable { 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(method, request); }) ); return this.sendHttpRequest(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(request: RequestData | string | null = null): Observable { return this.makeHttpRequest('get', this.getRequest(request)); } public post(request: RequestData | string | null = null): Observable { return this.makeHttpRequest('post', this.getRequest(request)); } public put(request: RequestData | string | null = null): Observable { return this.makeHttpRequest('put', this.getRequest(request)); } public delete(request: RequestData | string | null = null): Observable { return this.makeHttpRequest('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); } }