From d957d2d208d8f9ea4f76471358364469eb782c3f Mon Sep 17 00:00:00 2001 From: Nikolay <46225163+vigdorov@users.noreply.github.com> Date: Thu, 30 Jul 2020 08:32:56 +0300 Subject: [PATCH] =?UTF-8?q?HM-83.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D1=8B=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20?= =?UTF-8?q?=D1=81=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B5=D0=B9=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/AuthServiceAPI.js | 45 +++++++++++++++ src/api/HttpAPI.js | 92 +++++++++++++++++++++++++++++++ src/api/HttpAuthAPI.js | 109 +++++++++++++++++++++++++++++++++++++ src/api/LocalStorageAPI.js | 10 +++- src/api/TokenAPI.js | 56 +++++++++++++++++++ src/api/consts.js | 12 ++++ 6 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 src/api/AuthServiceAPI.js create mode 100644 src/api/HttpAPI.js create mode 100644 src/api/HttpAuthAPI.js create mode 100644 src/api/TokenAPI.js diff --git a/src/api/AuthServiceAPI.js b/src/api/AuthServiceAPI.js new file mode 100644 index 0000000..961510f --- /dev/null +++ b/src/api/AuthServiceAPI.js @@ -0,0 +1,45 @@ +import axios from 'axios'; + +import {AUTH_SERVICE, AUTH_ENDPOINTS} from './consts'; +import tokenApi from './TokenAPI'; + +/** + * Api для выполнения запросов авторизации + * @class AuthServiceApi + */ +class AuthServiceApi { + constructor () { + this.URL = AUTH_SERVICE; + } + + /** + * Выполняет запрос авторизации и если он успешен, то добавляет токены в память + * @param {string} login - логин пользователя + * @param {string} password - пароль пользователя + * @returns {Promise} + */ + auth = (login, password) => { + return axios.post(`${this.URL}${AUTH_ENDPOINTS.AUTH}`, { + login, + password, + }).then(({data: tokens}) => { + tokenApi.saveTokenPair(tokens); + }); + } + + /** + * Пытается реврешнуть пару токенов и в случае успеха обновляет их в памяти + * @returns {Promise} + */ + refresh = () => { + const refresh_token = tokenApi.getRefreshToken(); + return axios.post(`${this.URL}${AUTH_ENDPOINTS.REFRESH}`, {refresh_token}) + .then(({data: tokens}) => { + tokenApi.saveTokenPair(tokens); + }); + } +} + +const authServiceApi = new AuthServiceApi(); + +export default authServiceApi; diff --git a/src/api/HttpAPI.js b/src/api/HttpAPI.js new file mode 100644 index 0000000..f3201ce --- /dev/null +++ b/src/api/HttpAPI.js @@ -0,0 +1,92 @@ +import {makeUrlWithQuery} from '../utils/urlUtils'; +import httpAuthApi from './HttpAuthAPI'; + +const GET = 'GET'; +const POST = 'POST'; +const PUT = 'PUT'; +const DELETE = 'DELETE'; +const HEAD = 'HEAD'; +const OPTIONS = 'OPTIONS'; +const PATH = 'PATH'; + +/** + * Методы запросов к серверу + * @interface MethodType + * @type {'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'PATH'} + */ + +/** + * Api для выполнения запросов к серверу + * @class HttpApi + */ +class HttpApi { + /** + * Общий метод выполнения запроса + * @param {MethodType} method - метод запроса + * @param {string} url - адрес запроса + * @param {Object} body - тело запроса + */ + request = (method, url, body) => { + return httpAuthApi.request({method, url, body}); + } + + /** + * @param {string} url - адрес запроса + * @param {Object} query - параметры для передачи через query + */ + get = (url, query) => { + return this.request(GET, makeUrlWithQuery(url, query)); + } + + /** + * @param {string} url - адрес запроса + * @param {Object} body - тело запроса + */ + post = (url, body) => { + return this.request(POST, url, body); + } + + /** + * @param {string} url - адрес запроса + * @param {Object} body - тело запроса + */ + put = (url, body) => { + return this.request(PUT, url, body); + } + + /** + * @param {string} url - адрес запроса + * @param {Object} query - параметры для передачи через query + */ + delete = (url, query) => { + return this.request(DELETE, makeUrlWithQuery(url, query)); + } + + /** + * @param {string} url - адрес запроса + * @param {Object} query - параметры для передачи через query + */ + head = (url, query) => { + return this.request(HEAD, makeUrlWithQuery(url, query)); + } + + /** + * @param {string} url - адрес запроса + * @param {Object} query - параметры для передачи через query + */ + options = (url, query) => { + return this.request(OPTIONS, makeUrlWithQuery(url, query)); + } + + /** + * @param {string} url - адрес запроса + * @param {Object} body - тело запроса + */ + path = (url, body) => { + return this.request(PATH, url, body); + } +} + +const http = new HttpApi(); + +export default http; diff --git a/src/api/HttpAuthAPI.js b/src/api/HttpAuthAPI.js new file mode 100644 index 0000000..f9c201d --- /dev/null +++ b/src/api/HttpAuthAPI.js @@ -0,0 +1,109 @@ +import axios from 'axios'; +import authServiceApi from './AuthServiceAPI'; +import tokenApi from './TokenAPI'; + +/** + * @interface RequestConfig + * @type {Object} + * @property {MethodType} method - метод запроса + * @property {string} url - адрес запроса + * @property {Object} body - тело запроса + */ + +/** + * Api прикрепляет ко всем запросом заголовок с токеном авторизации. В случае, когда сервер + * @class HttpAuthApi + */ +class HttpAuthApi { + constructor () { + this.pendingRequests = []; + this.isPendingRefresh = false; + } + + /** + * Выполняет какое-то действие над всеми запросами из очереди, пока она не закончится + * @param {Function} handler - функция обработчик запроса + */ + processPendingRequests = (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); + }); + } + + /** + * Выбрасывает ошибки для всех запросов из очереди + * @param {axios.error} error - ошибка, которую возвращает axios + */ + rejectRequests = (error) => { + this.processPendingRequests(({reject}) => { + reject(error); + }); + } + + /** + * Перехватывает ошибку 401 (Не авторизован), другие ошибки выбрасывает дальше + * @param {axios.error} error - ошибка, которую возвращает axios + * @param {Function} resolve - функция промиса, которая разрешает его успешно + * @param {Function} reject - функция промиса, которая завершает его неудачей + * @param {RequestConfig} requestConfig - настройки для выполнения запроса + */ + errorHandling = (error, resolve, reject, requestConfig) => { + if (error.response.status === 401) { + this.isPendingRefresh = true; + this.pendingRequests({resolve, reject, requestConfig}); + authServiceApi.refresh() + .then(() => { + this.repeatRequests(); + }) + .catch((refreshError) => { + this.rejectRequests(refreshError); + /* + * Снять авторизацию и сделать роут + * Сбросить все запросы с ошибкой авторизации + */ + }); + } else { + reject(error); + } + } + + /** + * Добавляет к запросу токен в качестве заголовка + * @param {RequestConfig} requestConfig - настройки для выполнения запроса + */ + addTokenToRequest = (requestConfig) => ({ + ...requestConfig, + headers: { + 'Authorization': tokenApi.getAccessToken(), + }, + }) + + /** + * Метод для внешнего использования, выполняет запрос к серверу, используя axios + * @param {RequestConfig} requestConfig - настройки для выполнения запроса + */ + request = (requestConfig) => { + return new Promise((resolve, reject) => { + 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/api/LocalStorageAPI.js b/src/api/LocalStorageAPI.js index 4cba8ef..31e6a58 100644 --- a/src/api/LocalStorageAPI.js +++ b/src/api/LocalStorageAPI.js @@ -1,11 +1,15 @@ +import {LOCAL_STORAGE_TYPE} from './consts'; + /** * Класс работы с Local Storage браузера * @class LocalStorageAPI - * @param {string} key - ключ по которому будет хранится информация + * @param {string} key - уникальный ключ для local или session storage + * @param {'LOCAL' | 'SESSION'} type - тип storage */ class LocalStorageAPI { - constructor (key) { + constructor (key, type = LOCAL_STORAGE_TYPE.LOCAL) { this.key = key; + this.api = type === LOCAL_STORAGE_TYPE.LOCAL ? localStorage : sessionStorage; } /** @@ -18,7 +22,7 @@ class LocalStorageAPI { /** * Записывает данные в Local Storage по ключу из конструктора - * @param {*} value - значение в Local Storage + * @param {Object} value - значение в Local Storage */ createOrUpdate (value) { localStorage.setItem(this.key, JSON.stringify(value)); diff --git a/src/api/TokenAPI.js b/src/api/TokenAPI.js new file mode 100644 index 0000000..c328844 --- /dev/null +++ b/src/api/TokenAPI.js @@ -0,0 +1,56 @@ +import LocalStorageAPI from './LocalStorageAPI'; + +const API_NAME = 'storageServiceUITokenApi'; + +/** + * @interface TokenPair + * @type {Object} + * @property {string} access_token - токен авторизации + * @property {string} refresh_token = токен рефреша авторизации + */ + +/** + * Api сохраняет и возвращает токены авторизации + * @class TokenApi + */ +class TokenApi { + constructor () { + this.localApi = new LocalStorageAPI(API_NAME); + this.sessionApi = new LocalStorageAPI(API_NAME); + } + + /** + * Сохраняет пару токенов + * @param {TokenPair} tokens - пара токенов + */ + saveTokenPair = (tokens) => { + this.localApi.createOrUpdate(tokens.refresh_token); + this.sessionApi.createOrUpdate(tokens.access_token); + } + + /** + * Возвращает токен авторизации + */ + getAccessToken = () => { + return this.localApi.request(); + } + + /** + * Возвращает токен ревреша авторизации + */ + getRefreshToken = () => { + return this.sessionApi.request(); + } + + /** + * Очищает токены из памяти + */ + clearTokents = () => { + this.localApi.remove(); + this.sessionApi.remove(); + } +} + +const tokenApi = new TokenApi(); + +export default tokenApi; diff --git a/src/api/consts.js b/src/api/consts.js index 86dc3b6..de9464a 100644 --- a/src/api/consts.js +++ b/src/api/consts.js @@ -34,3 +34,15 @@ export const ENDPOINTS = { export const TESTING_HEADERS = { 'Api-Name': 'store-service-test', }; + +export const AUTH_SERVICE = 'http://api.auth.vigdorov.ru'; + +export const AUTH_ENDPOINTS = { + AUTH: '/auth', + REFRESH: '/refresh', +}; + +export const LOCAL_STORAGE_TYPE = { + LOCAL: 'LOCAL', + SESSION: 'SESSION', +};