diff --git a/src/api/api.service.ts b/src/api/api.service.ts index a53c3d9..f2ebf95 100644 --- a/src/api/api.service.ts +++ b/src/api/api.service.ts @@ -1,10 +1,12 @@ -import {catchError, mergeMap, Observable, retryWhen, tap, timer} from "rxjs"; +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) => @@ -35,13 +37,14 @@ export enum AvailableVersion { @Injectable() export default abstract class ApiService implements SetRequestBuilderAfterBuild { - constructor(private http: HttpClient, private notify: OpenNotifyService, private router: Router) { + 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; @@ -128,6 +131,18 @@ export default abstract class ApiService implements SetRequestBuilderAfterBuild 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")) { @@ -147,6 +162,7 @@ export default abstract class ApiService implements SetRequestBuilderAfterBuild message = 'Ошибка запроса. Пожалуйста, проверьте отправленные данные.'; break; case 401: + this.router.navigate(['/login/']).then(); message = 'Ошибка авторизации. Пожалуйста, выполните вход с правильными учетными данными.'; break; case 403: diff --git a/src/app/app.component.ts b/src/app/app.component.ts index e5e41f5..b13707f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -4,6 +4,7 @@ import {FooterComponent} from "@component/common/footer/footer.component"; import localeRu from '@angular/common/locales/ru'; import { registerLocaleData } from '@angular/common'; import {FocusNextDirective} from "@/directives/focus-next.directive"; +import {TokenRefreshService} from "@service/token-refresh.service"; @Component({ selector: 'app-root', diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts new file mode 100644 index 0000000..0a23f37 --- /dev/null +++ b/src/services/auth.service.ts @@ -0,0 +1,88 @@ +import {EventEmitter, Injectable} from '@angular/core'; +import {HttpClient, HttpHeaders} from "@angular/common/http"; +import {catchError, Observable, of, tap} from "rxjs"; +import {TokenResponse} from "@api/v1/tokenResponse"; +import ApiService from "@api/api.service"; + +export enum AvailableAuthenticationProvider { + Bearer +} + +export class AuthToken { + accessToken: string; + expiresIn: Date; + authProvider: AvailableAuthenticationProvider; + endpoint: string; + + constructor(accessToken: string, expiresIn: Date, authProvider: AvailableAuthenticationProvider, refreshEndpoint: string) { + this.accessToken = accessToken; + this.expiresIn = expiresIn; + this.authProvider = authProvider; + this.endpoint = refreshEndpoint; + } + + static httpHeader(token: AuthToken): HttpHeaders { + let header = new HttpHeaders(); + + if (token.authProvider === AvailableAuthenticationProvider.Bearer) + header = header.set('Authorization', `Bearer ${token.accessToken}`); + + return header; + } +} + +@Injectable({ + providedIn: 'root', +}) +export class AuthService { + public expireTokenChange = new EventEmitter(); + public tokenChangeError = new EventEmitter(); + + constructor(private http: HttpClient) { + } + + public static setToken(token: TokenResponse, provider: AvailableAuthenticationProvider, refreshEndpoint: string) { + localStorage.setItem(ApiService.tokenKey, JSON.stringify( + new AuthToken(token.accessToken, token.expiresIn, provider, refreshEndpoint) + )); + } + + public static get tokenExpiresIn(): Date { + const token = localStorage.getItem(ApiService.tokenKey); + + if (!token) + return new Date(); + + const result = new Date((JSON.parse(token) as AuthToken).expiresIn); + return result <= new Date() ? new Date() : result; + } + + public refreshToken(): Observable { + const token = localStorage.getItem(ApiService.tokenKey); + + if (!token) + return of(); + + const authToken = JSON.parse(token) as AuthToken; + + switch (authToken.authProvider) { + case AvailableAuthenticationProvider.Bearer: + return this.http.get(authToken.endpoint, {withCredentials: true}) + .pipe( + catchError(error => { + this.tokenChangeError.emit(); + throw error; + }), + tap(response => { + const newExpireDate = new Date(response.expiresIn); + const oldExpireDate = new Date(authToken.expiresIn); + + if (newExpireDate.getTime() !== oldExpireDate.getTime()) { + AuthService.setToken(response, AvailableAuthenticationProvider.Bearer, authToken.endpoint); + this.expireTokenChange.emit(newExpireDate); + } + }) + ); + } + } +}