import { BehaviorSubject, catchError, distinctUntilChanged, filter, first, Observable, of, ReplaySubject, switchMap } from "rxjs"; import {HttpClient, HttpErrorResponse} from "@angular/common/http"; import {environment} from "@environment"; import {Router} from "@angular/router"; import {Injectable} from "@angular/core"; import {RequestBuilder, RequestData} from "@api/RequestBuilder"; import {ToastrService} from "ngx-toastr"; import {AuthRoles} from "@model/authRoles"; export enum AvailableVersion { v1 } @Injectable() export default abstract class ApiService { constructor(protected http: HttpClient, protected notify: ToastrService, private router: Router) { } private apiUrl = environment.apiUrl; private static isRefreshingToken: BehaviorSubject = new BehaviorSubject(false); private static refreshTokenSubject: ReplaySubject = new ReplaySubject(1); protected abstract basePath: string; protected abstract version: AvailableVersion; 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 || 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, secondTry: boolean = false): Observable { const doneEndpoint = this.combinedUrl(request); return this.http.request(method, doneEndpoint, { withCredentials: request.withCredentials, headers: request.httpHeaders, body: request.data, }).pipe( catchError(error => { if (request.needAuth && !secondTry && error.status === 401) return this.handle401Error(error).pipe( switchMap(() => this.sendHttpRequest(method, request, true)) ); else { if (!request.silenceMode) this.handleError(error); throw error; } }) ); } private refreshToken(): Observable { return this.http.get(ApiService.combineUrls(this.apiUrl, AvailableVersion[AvailableVersion.v1], 'Auth', 'ReLogin'), { withCredentials: true }); } private handle401Error(error: any): Observable { if (ApiService.isRefreshingToken.value) return ApiService.refreshTokenSubject.asObservable(); ApiService.isRefreshingToken.next(true); return this.refreshToken().pipe( switchMap(_ => { ApiService.isRefreshingToken.next(false); ApiService.refreshTokenSubject.next(null); return of(null); }), catchError(err => { ApiService.isRefreshingToken.next(false); ApiService.refreshTokenSubject.error(err); ApiService.refreshTokenSubject = new ReplaySubject(1); throw error; }) ); } private makeHttpRequest(method: 'get' | 'post' | 'delete' | 'put', request: RequestData): Observable { if (request.needAuth) { return ApiService.isRefreshingToken.pipe( distinctUntilChanged(), filter(isRefreshing => !isRefreshing), first(), switchMap(() => this.sendHttpRequest(method, request)) ); } else { 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; request.withCredentials = true; return this; } private handleError(error: HttpErrorResponse): void { // todo: change to Retry-After condition if (error.error && error.error.detail && error.error.detail.includes("setup")) { this.router.navigate(['/setup/']).then(); return; } let title: string; let message: string | undefined = undefined; if (error.error instanceof ErrorEvent) { title = `Произошла ошибка: ${error.error.message}`; } else { if (error.error && error.error.type && error.error.title) { title = error.error.title || `Ошибка с кодом ${error.status}`; message = error.error.detail || 'Неизвестная ошибка'; } else { switch (error.status) { case 0: title = 'Неизвестная ошибка. Пожалуйста, попробуйте позже.'; break; case 400: title = 'Ошибка запроса. Пожалуйста, проверьте отправленные данные.'; break; case 401: this.router.navigate(['/login/']).then(); title = 'Ошибка авторизации. Пожалуйста, выполните вход с правильными учетными данными.'; break; case 403: title = 'Отказано в доступе. У вас нет разрешения на выполнение этого действия.'; break; case 404: title = 'Запрашиваемый ресурс не найден.'; break; case 500: title = 'Внутренняя ошибка сервера. Пожалуйста, попробуйте позже.'; break; case 503: title = 'Сервер на обслуживании. Пожалуйста, попробуйте позже.'; break; default: title = `Сервер вернул код ошибки: ${error.status}`; break; } } if (!message) message = error.error.statusMessage; } this.notify.error(message == '' ? undefined : message, title); } }