refactor: adapting token storage to the API
This commit is contained in:
parent
9209b31db2
commit
2b09086902
@ -1,35 +1,21 @@
|
|||||||
import {catchError, distinctUntilChanged, filter, first, mergeMap, Observable, retryWhen, switchMap, timer} from "rxjs";
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
catchError,
|
||||||
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
|
first,
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
ReplaySubject,
|
||||||
|
switchMap
|
||||||
|
} from "rxjs";
|
||||||
import {HttpClient, HttpErrorResponse} from "@angular/common/http";
|
import {HttpClient, HttpErrorResponse} from "@angular/common/http";
|
||||||
import {environment} from "@environment";
|
import {environment} from "@environment";
|
||||||
import {Router} from "@angular/router";
|
import {Router} from "@angular/router";
|
||||||
import {Injectable} from "@angular/core";
|
import {Injectable} from "@angular/core";
|
||||||
import {RequestBuilder, RequestData} from "@api/RequestBuilder";
|
import {RequestBuilder, RequestData} from "@api/RequestBuilder";
|
||||||
import {TokenRefreshService} from "@service/token-refresh.service";
|
|
||||||
import {AuthToken} from "@service/auth.service";
|
|
||||||
import {ToastrService} from "ngx-toastr";
|
import {ToastrService} from "ngx-toastr";
|
||||||
|
import {AuthRoles} from "@model/AuthRoles";
|
||||||
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 {
|
export enum AvailableVersion {
|
||||||
v1
|
v1
|
||||||
@ -37,16 +23,16 @@ export enum AvailableVersion {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default abstract class ApiService {
|
export default abstract class ApiService {
|
||||||
constructor(private http: HttpClient, private notify: ToastrService, private router: Router, protected tokenRefreshService: TokenRefreshService) {
|
constructor(private http: HttpClient, private notify: ToastrService, private router: Router) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private apiUrl = environment.apiUrl;
|
private apiUrl = environment.apiUrl;
|
||||||
|
private static isRefreshingToken: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
private static refreshTokenSubject: ReplaySubject<any> = new ReplaySubject(1);
|
||||||
|
|
||||||
protected abstract basePath: string;
|
protected abstract basePath: string;
|
||||||
protected abstract version: AvailableVersion;
|
protected abstract version: AvailableVersion;
|
||||||
|
|
||||||
public static readonly tokenKey = 'auth_token';
|
|
||||||
|
|
||||||
private static addQuery(endpoint: string, queryParams?: Record<string, string | number | boolean | Array<any> | null> | null): string {
|
private static addQuery(endpoint: string, queryParams?: Record<string, string | number | boolean | Array<any> | null> | null): string {
|
||||||
const url = new URL(endpoint);
|
const url = new URL(endpoint);
|
||||||
|
|
||||||
@ -73,7 +59,7 @@ export default abstract class ApiService {
|
|||||||
return ApiService.addQuery(ApiService.combineUrls(this.apiUrl, AvailableVersion[this.version], this.basePath, request.endpoint), request.queryParams);
|
return ApiService.addQuery(ApiService.combineUrls(this.apiUrl, AvailableVersion[this.version], this.basePath, request.endpoint), request.queryParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendHttpRequest<Type>(method: 'get' | 'post' | 'delete' | 'put', request: RequestData): Observable<Type> {
|
private sendHttpRequest<Type>(method: 'get' | 'post' | 'delete' | 'put', request: RequestData, secondTry: boolean = false): Observable<Type> {
|
||||||
const doneEndpoint = this.combinedUrl(request);
|
const doneEndpoint = this.combinedUrl(request);
|
||||||
|
|
||||||
return this.http.request<Type>(method, doneEndpoint, {
|
return this.http.request<Type>(method, doneEndpoint, {
|
||||||
@ -81,36 +67,59 @@ export default abstract class ApiService {
|
|||||||
headers: request.httpHeaders,
|
headers: request.httpHeaders,
|
||||||
body: request.data
|
body: request.data
|
||||||
}).pipe(
|
}).pipe(
|
||||||
retryWithInterval<Type>(),
|
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
|
if (!secondTry && error.status === 401)
|
||||||
|
return this.handle401Error().pipe(
|
||||||
|
switchMap(() => this.sendHttpRequest<Type>(method, request, true))
|
||||||
|
);
|
||||||
|
else {
|
||||||
if (!request.silenceMode)
|
if (!request.silenceMode)
|
||||||
this.handleError(error);
|
this.handleError(error);
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshToken(): Observable<AuthRoles> {
|
||||||
|
return this.http.get<AuthRoles>(ApiService.combineUrls(this.apiUrl, AvailableVersion[AvailableVersion.v1], 'Auth', 'ReLogin'), {
|
||||||
|
withCredentials: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handle401Error(): Observable<any> {
|
||||||
|
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 err;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private makeHttpRequest<Type>(method: 'get' | 'post' | 'delete' | 'put', request: RequestData): Observable<Type> {
|
private makeHttpRequest<Type>(method: 'get' | 'post' | 'delete' | 'put', request: RequestData): Observable<Type> {
|
||||||
if (request.needAuth)
|
if (request.needAuth) {
|
||||||
return this.tokenRefreshService.getTokenRefreshing$().pipe(
|
return ApiService.isRefreshingToken.pipe(
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
filter(isRefreshing => !isRefreshing),
|
filter(isRefreshing => !isRefreshing),
|
||||||
first(),
|
first(),
|
||||||
switchMap(() => {
|
switchMap(() => this.sendHttpRequest<Type>(method, request))
|
||||||
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<Type>(method, request);
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
return this.sendHttpRequest<Type>(method, request);
|
return this.sendHttpRequest<Type>(method, request);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getRequest(request: RequestData | string | null): RequestData {
|
private getRequest(request: RequestData | string | null): RequestData {
|
||||||
if (request === null)
|
if (request === null)
|
||||||
@ -143,6 +152,7 @@ export default abstract class ApiService {
|
|||||||
|
|
||||||
public addAuth(request: RequestData) {
|
public addAuth(request: RequestData) {
|
||||||
request.needAuth = true;
|
request.needAuth = true;
|
||||||
|
request.withCredentials = true;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,8 +197,10 @@ export default abstract class ApiService {
|
|||||||
}
|
}
|
||||||
if (error.error?.Error)
|
if (error.error?.Error)
|
||||||
message = error.error.Error;
|
message = error.error.Error;
|
||||||
|
else if (error.error instanceof String)
|
||||||
|
message = error.error.toString();
|
||||||
else
|
else
|
||||||
message = error.error;
|
message = error.error.statusMessage;
|
||||||
}
|
}
|
||||||
this.notify.error(message == '' ? undefined : message, title);
|
this.notify.error(message == '' ? undefined : message, title);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import {Injectable} from "@angular/core";
|
import {Injectable} from "@angular/core";
|
||||||
import ApiService, {AvailableVersion} from "@api/api.service";
|
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||||
import {LoginRequest} from "@api/v1/loginRequest";
|
import {LoginRequest} from "@api/v1/loginRequest";
|
||||||
import {TokenResponse} from "@api/v1/tokenResponse";
|
import {catchError, of} from "rxjs";
|
||||||
import {catchError, of, tap} from "rxjs";
|
|
||||||
import {AuthRoles} from "@model/AuthRoles";
|
import {AuthRoles} from "@model/AuthRoles";
|
||||||
import {AuthService, AvailableAuthenticationProvider} from "@service/auth.service";
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class AuthApiService extends ApiService {
|
export default class AuthApiService extends ApiService {
|
||||||
@ -18,13 +16,16 @@ export default class AuthApiService extends ApiService {
|
|||||||
.setWithCredentials()
|
.setWithCredentials()
|
||||||
.build;
|
.build;
|
||||||
|
|
||||||
return this.post<TokenResponse>(request)
|
return this.post<AuthRoles>(request);
|
||||||
.pipe(
|
}
|
||||||
tap(response => {
|
|
||||||
AuthService.setToken(response, AvailableAuthenticationProvider.Bearer, this.combinedUrl(this.createRequestBuilder().setEndpoint('ReLogin').build));
|
public reLogin(){
|
||||||
this.tokenRefreshService.setRefreshTokenExpireMs(response.expiresIn);
|
let request = this.createRequestBuilder()
|
||||||
})
|
.setEndpoint('ReLogin')
|
||||||
);
|
.setWithCredentials()
|
||||||
|
.build;
|
||||||
|
|
||||||
|
return this.get<AuthRoles>(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public logout() {
|
public logout() {
|
||||||
@ -33,13 +34,7 @@ export default class AuthApiService extends ApiService {
|
|||||||
.setEndpoint('Logout')
|
.setEndpoint('Logout')
|
||||||
.build;
|
.build;
|
||||||
|
|
||||||
return this.addAuth(request)
|
return this.addAuth(request).get(request);
|
||||||
.get(request)
|
|
||||||
.pipe(
|
|
||||||
tap(_ => {
|
|
||||||
localStorage.removeItem(ApiService.tokenKey);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRole(isSilence: boolean = true) {
|
public getRole(isSilence: boolean = true) {
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
import {Injectable} from '@angular/core';
|
|
||||||
import {HttpClient, HttpHeaders} from "@angular/common/http";
|
|
||||||
import {map, Observable, throwError} 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public 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 {
|
|
||||||
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<Date> {
|
|
||||||
const token = localStorage.getItem(ApiService.tokenKey);
|
|
||||||
|
|
||||||
if (!token)
|
|
||||||
return throwError(() => new Error("Token is not found"));
|
|
||||||
|
|
||||||
const authToken = JSON.parse(token) as AuthToken;
|
|
||||||
|
|
||||||
switch (authToken.authProvider) {
|
|
||||||
case AvailableAuthenticationProvider.Bearer:
|
|
||||||
return this.http.get<TokenResponse>(authToken.endpoint, {withCredentials: true})
|
|
||||||
.pipe(
|
|
||||||
map(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);
|
|
||||||
return newExpireDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newExpireDate;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user