diff --git a/src/components/user/actions.js b/src/components/user/actions.js index 079f6ac..9d21232 100644 --- a/src/components/user/actions.js +++ b/src/components/user/actions.js @@ -1,6 +1,5 @@ import { routeActions } from 'react-router-redux'; -import request from 'services/request'; import captcha from 'services/captcha'; import accounts from 'services/api/accounts'; import authentication from 'services/api/authentication'; @@ -92,15 +91,8 @@ export function acceptRules() { ; } -let middlewareAdded = false; export function authenticate(token, refreshToken) { // TODO: this action, probably, belongs to components/auth return (dispatch, getState) => { - if (!middlewareAdded) { - request.addMiddleware(tokenCheckMiddleware(dispatch, getState)); - request.addMiddleware(tokenApplyMiddleware(dispatch, getState)); - middlewareAdded = true; - } - refreshToken = refreshToken || getState().user.refreshToken; dispatch(updateUser({ token, @@ -116,113 +108,3 @@ export function authenticate(token, refreshToken) { // TODO: this action, probab }; } -function requestAccessToken(refreshToken, dispatch) { - let promise; - if (refreshToken) { - promise = authentication.refreshToken(refreshToken); - } else { - promise = Promise.reject(); - } - - return promise - .then((resp) => dispatch(updateUser({ - token: resp.access_token - }))) - .catch(() => dispatch(logout())); -} - -/** - * Ensures, that all user's requests have fresh access token - * - * @param {function} dispatch - * @param {function} getState - * - * @return {object} middleware - */ -function tokenCheckMiddleware(dispatch, getState) { - return { - before(data) { - const {isGuest, refreshToken, token} = getState().user; - const isRefreshTokenRequest = data.url.includes('refresh-token'); - - if (isGuest || isRefreshTokenRequest || !token) { - return data; - } - - const SAFETY_FACTOR = 60; // ask new token earlier to overcome time dissynchronization problem - const jwt = getJWTPayload(token); - - if (jwt.exp - SAFETY_FACTOR < Date.now() / 1000) { - return requestAccessToken(refreshToken, dispatch).then(() => data); - } - - return data; - }, - - catch(resp, restart) { - /* - { - "name": "Unauthorized", - "message": "You are requesting with an invalid credential.", - "code": 0, - "status": 401, - "type": "yii\\web\\UnauthorizedHttpException" - } - { - "name": "Unauthorized", - "message": "Token expired", - "code": 0, - "status": 401, - "type": "yii\\web\\UnauthorizedHttpException" - } - */ - if (resp && resp.status === 401) { - const {refreshToken} = getState().user; - if (resp.message === 'Token expired' && refreshToken) { - // request token and retry - return requestAccessToken(refreshToken, dispatch).then(restart); - } - - dispatch(logout()); - } - - return Promise.reject(resp); - } - }; -} - -/** - * Applies Bearer header for all requests - * - * @param {function} dispatch - * @param {function} getState - * - * @return {object} middleware - */ -function tokenApplyMiddleware(dispatch, getState) { - return { - before(data) { - const {token} = getState().user; - - if (token) { - data.options.headers.Authorization = `Bearer ${token}`; - } - - return data; - } - }; -} - -function getJWTPayload(jwt) { - const parts = (jwt || '').split('.'); - - if (parts.length !== 3) { - throw new Error('Invalid jwt token'); - } - - try { - return JSON.parse(atob(parts[1])); - } catch (err) { - throw new Error('Can not decode jwt token'); - } -} diff --git a/src/components/user/factory.js b/src/components/user/factory.js index 9449e7b..70788b6 100644 --- a/src/components/user/factory.js +++ b/src/components/user/factory.js @@ -1,13 +1,20 @@ import { authenticate, changeLang } from 'components/user/actions'; +import request from 'services/request'; +import bearerHeaderMiddleware from './middlewares/bearerHeaderMiddleware'; +import refreshTokenMiddleware from './middlewares/refreshTokenMiddleware'; + /** * Initializes User state with the fresh data * * @param {object} store - redux store * - * @return {Promise} a promise, that resolves in User state + * @return {Promise} - a promise, that resolves in User state */ export function factory(store) { + request.addMiddleware(refreshTokenMiddleware(store)); + request.addMiddleware(bearerHeaderMiddleware(store)); + return new Promise((resolve, reject) => { const {user} = store.getState(); diff --git a/src/components/user/middlewares/bearerHeaderMiddleware.js b/src/components/user/middlewares/bearerHeaderMiddleware.js new file mode 100644 index 0000000..caca14a --- /dev/null +++ b/src/components/user/middlewares/bearerHeaderMiddleware.js @@ -0,0 +1,21 @@ +/** + * Applies Bearer header for all requests + * + * @param {object} store - redux store + * @param {function} store.getState + * + * @return {object} - request middleware + */ +export default function bearerHeaderMiddleware({getState}) { + return { + before(data) { + const {token} = getState().user; + + if (token) { + data.options.headers.Authorization = `Bearer ${token}`; + } + + return data; + } + }; +} diff --git a/src/components/user/middlewares/refreshTokenMiddleware.js b/src/components/user/middlewares/refreshTokenMiddleware.js new file mode 100644 index 0000000..712c684 --- /dev/null +++ b/src/components/user/middlewares/refreshTokenMiddleware.js @@ -0,0 +1,93 @@ +import authentication from 'services/api/authentication'; +import {updateUser, logout} from '../actions'; + +/** + * Ensures, that all user's requests have fresh access token + * + * @param {object} store - redux store + * @param {function} store.getState + * @param {function} store.dispatch + * + * @return {object} - request middleware + */ +export default function refreshTokenMiddleware({dispatch, getState}) { + return { + before(data) { + const {isGuest, refreshToken, token} = getState().user; + const isRefreshTokenRequest = data.url.includes('refresh-token'); + + if (isGuest || isRefreshTokenRequest || !token) { + return data; + } + + const SAFETY_FACTOR = 60; // ask new token earlier to overcome time dissynchronization problem + const jwt = getJWTPayload(token); + + if (jwt.exp - SAFETY_FACTOR < Date.now() / 1000) { + return requestAccessToken(refreshToken, dispatch).then(() => data); + } + + return data; + }, + + catch(resp, restart) { + /* + { + "name": "Unauthorized", + "message": "You are requesting with an invalid credential.", + "code": 0, + "status": 401, + "type": "yii\\web\\UnauthorizedHttpException" + } + { + "name": "Unauthorized", + "message": "Token expired", + "code": 0, + "status": 401, + "type": "yii\\web\\UnauthorizedHttpException" + } + */ + if (resp && resp.status === 401) { + const {refreshToken} = getState().user; + if (resp.message === 'Token expired' && refreshToken) { + // request token and retry + return requestAccessToken(refreshToken, dispatch).then(restart); + } + + dispatch(logout()); + } + + return Promise.reject(resp); + } + }; +} + +function requestAccessToken(refreshToken, dispatch) { + let promise; + if (refreshToken) { + promise = authentication.refreshToken(refreshToken); + } else { + promise = Promise.reject(); + } + + return promise + .then((resp) => dispatch(updateUser({ + token: resp.access_token + }))) + .catch(() => dispatch(logout())); +} + + +function getJWTPayload(jwt) { + const parts = (jwt || '').split('.'); + + if (parts.length !== 3) { + throw new Error('Invalid jwt token'); + } + + try { + return JSON.parse(atob(parts[1])); + } catch (err) { + throw new Error('Can not decode jwt token'); + } +}