diff --git a/public/index.html b/public/index.html index d1c1bb9..cbdc28c 100644 --- a/public/index.html +++ b/public/index.html @@ -7,7 +7,7 @@ -
= Omit& { }; const requestMiddleware = async (config: RequestConfig): Promise=> { - const axiosResponse = await axios.request (config); + const axiosResponse = await httpAuthApi.request (config); // Добавить обработку ошибок return axiosResponse.data; }; diff --git a/src/core/infrastructure/HttpAuthAPI.ts b/src/core/infrastructure/HttpAuthAPI.ts new file mode 100644 index 0000000..b9f57cb --- /dev/null +++ b/src/core/infrastructure/HttpAuthAPI.ts @@ -0,0 +1,92 @@ +import axios, {AxiosError, AxiosRequestConfig, AxiosResponse} from 'axios'; +import authServiceApi from './AuthServiceAPI'; +import {tokenAPI} from './TokenAPI'; + +type HandlerArgs = { + resolve: (v: AxiosResponse ) => void, + reject: (reason?: any) => void, + requestConfig: AxiosRequestConfig + error?: AxiosError , +} + +type Handler = (args: HandlerArgs ) => void; + +class HttpAuthApi { + pendingRequests: HandlerArgs []; + + isPendingRefresh: boolean; + + constructor() { + this.pendingRequests = []; + this.isPendingRefresh = false; + } + + processPendingRequests = (handler: Handler ) => { + this.isPendingRefresh = false; + while (this.pendingRequests.length) { + const pendingRequest = this.pendingRequests.shift()!; + handler(pendingRequest); + } + } + + repeatRequests = () => { + this.processPendingRequests(({resolve, reject, requestConfig}) => { + this.request(requestConfig) + .then(resolve) + .catch(reject); + }); + } + + rejectRequests = (error: AxiosError) => { + this.processPendingRequests(({reject}) => { + reject(error); + }); + } + + errorHandling = ( + error: AxiosError , + resolve: (v: AxiosResponse ) => void, + reject: (reason?: any) => void, + requestConfig: AxiosRequestConfig + ) => { + if (error.response?.status === 401) { + this.isPendingRefresh = true; + this.pendingRequests.push({resolve, reject, requestConfig}); + authServiceApi.refresh() + .then(() => { + this.repeatRequests(); + }) + .catch((refreshError: AxiosError) => { + this.rejectRequests(refreshError); + tokenAPI.clearTokents(); + location.reload(); + }); + } else { + reject(error); + } + } + + addTokenToRequest = (requestConfig: AxiosRequestConfig): AxiosRequestConfig => ({ + ...requestConfig, + headers: { + ...(requestConfig.headers ?? {}), + 'Authorization': tokenAPI.getAccessToken(), + }, + }) + + request = (requestConfig: AxiosRequestConfig): Promise > => { + return new Promise((resolve, reject) => { + if (this.isPendingRefresh) { + this.pendingRequests.push({resolve, reject, requestConfig}); + } else { + axios(this.addTokenToRequest(requestConfig)) + .then(response => resolve(response)) + .catch(error => this.errorHandling(error, resolve, reject, requestConfig)); + } + }); + } +} + +const httpAuthApi = new HttpAuthApi(); + +export default httpAuthApi; diff --git a/src/core/infrastructure/StorageAPI.ts b/src/core/infrastructure/StorageAPI.ts new file mode 100644 index 0000000..971e552 --- /dev/null +++ b/src/core/infrastructure/StorageAPI.ts @@ -0,0 +1,35 @@ +export class StorageAPI { + private _init: T; + + private _storage: Storage; + + private _stateName: string; + + constructor(init: T, stateName: string, storageType: 'local' | 'session' = 'local') { + this._init = init; + this._stateName = stateName; + this._storage = storageType === 'local' ? localStorage : sessionStorage; + + if (!this._storage.getItem(stateName)) { + this.set(this._init); + } + } + + set(updatedState: T) { + this._storage.setItem(this._stateName, JSON.stringify(updatedState)); + } + + get(): T { + const stringValue = this._storage.getItem(this._stateName) || ''; + + try { + return JSON.parse(stringValue); + } catch (e) { + return this._init; + } + } + + clear() { + this._storage.removeItem(this._stateName); + } +} diff --git a/src/core/infrastructure/TokenAPI.ts b/src/core/infrastructure/TokenAPI.ts new file mode 100644 index 0000000..2898841 --- /dev/null +++ b/src/core/infrastructure/TokenAPI.ts @@ -0,0 +1,32 @@ +import {StorageAPI} from './StorageAPI'; + +class TokenAPI { + private accessTokenAPI: StorageAPI ; + + private refreshTokenAPI: StorageAPI ; + + constructor() { + this.accessTokenAPI = new StorageAPI('', 'access', 'session'); + this.refreshTokenAPI = new StorageAPI('', 'refresh', 'local'); + } + + setTokens(accessToken: string, refreshToken: string) { + this.accessTokenAPI.set(accessToken); + this.refreshTokenAPI.set(refreshToken); + } + + getAccessToken() { + return this.accessTokenAPI.get(); + } + + getRefreshToken() { + return this.refreshTokenAPI.get(); + } + + clearTokents() { + this.accessTokenAPI.clear(); + this.refreshTokenAPI.clear(); + } +} + +export const tokenAPI = new TokenAPI(); diff --git a/src/core/infrastructure/atom/authAtom.ts b/src/core/infrastructure/atom/authAtom.ts new file mode 100644 index 0000000..906cc8a --- /dev/null +++ b/src/core/infrastructure/atom/authAtom.ts @@ -0,0 +1,14 @@ +import {declareAction, declareAtom} from '@reatom/core'; +import {store} from './store'; + +const authAction = declareAction (); + +export const authAtom = declareAtom(false, on => [ + on(authAction, (_state, payload) => payload), +]); + +export const bindedActions = { + changeAuth: (isAuth: boolean) => { + store.dispatch(authAction(isAuth)); + }, +}; diff --git a/src/core/infrastructure/atom/exampleAtom.ts b/src/core/infrastructure/atom/exampleAtom.ts deleted file mode 100644 index 56ab85b..0000000 --- a/src/core/infrastructure/atom/exampleAtom.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {declareAction, declareAtom} from '@reatom/core'; - -export const changeNameAction = declareAction (); - -export const nameAtom = declareAtom('', on => [ - on(changeNameAction, (_state, payload) => payload), -]); diff --git a/src/core/services/LocalStorageService.ts b/src/core/services/LocalStorageService.ts deleted file mode 100644 index ddc19b1..0000000 --- a/src/core/services/LocalStorageService.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const makeLocalStorageService = (init: T, stateName: string) => { - if (!localStorage.getItem(stateName)) { - localStorage.setItem(stateName, JSON.stringify(init)); - } - - return { - set: (updatedState: T) => { - localStorage.setItem(stateName, JSON.stringify(updatedState)); - return updatedState; - }, - get: (): T => { - const stringValue = localStorage.getItem(stateName) || ''; - - try { - return JSON.parse(stringValue); - } catch (e) { - return init; - } - }, - }; -}; diff --git a/src/core/utils/__test__/jsonParse.test.ts b/src/core/utils/__test__/jsonParse.test.ts index f75f886..f5b47c3 100644 --- a/src/core/utils/__test__/jsonParse.test.ts +++ b/src/core/utils/__test__/jsonParse.test.ts @@ -16,7 +16,7 @@ describe('jsonParse', () => { }); it('Должен вернуть undefined для не корректных значений', () => { - expect(jsonParse()).toBeUndefined(); + expect(jsonParse(undefined)).toBeUndefined(); expect(jsonParse('')).toBeUndefined(); expect(jsonParse(' ')).toBeUndefined(); expect(jsonParse('{"9')).toBeUndefined(); diff --git a/src/core/utils/jsonParse.ts b/src/core/utils/jsonParse.ts index 73f2176..c9915a6 100644 --- a/src/core/utils/jsonParse.ts +++ b/src/core/utils/jsonParse.ts @@ -1,4 +1,6 @@ -export const jsonParse = (str?: string, defaultValue?: T): Undefinable => { +export function jsonParse (str: Undefinable , defaultValue: T): T; +export function jsonParse (str: Undefinable , defaultValue?: T): Undefinable ; +export function jsonParse (str: Undefinable , defaultValue?: T) { const trimStr = str?.trim(); try { const parsedValue = JSON.parse(trimStr ?? ''); @@ -7,4 +9,4 @@ export const jsonParse = (str?: string, defaultValue?: T): Undefinable => } catch (e) { return defaultValue; } -}; +}