#126: middleware feature for request service. Created middlewares for token headers and token refreshing

This commit is contained in:
SleepWalker 2016-06-04 19:54:42 +03:00
parent 3bf02f16dc
commit 35d430e13c
2 changed files with 149 additions and 71 deletions

View File

@ -54,38 +54,12 @@ export function logout() {
}
export function fetchUserData() {
return (dispatch, getState) =>
return (dispatch) =>
accounts.current()
.then((resp) => {
dispatch(updateUser(resp));
return dispatch(changeLang(resp.lang));
})
.catch((resp) => {
/*
{
"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 {token} = getState().user;
if (resp.message === 'Token expired' && token) {
return dispatch(authenticate(token));
}
dispatch(logout());
}
});
}
@ -109,32 +83,128 @@ export function changePassword({
;
}
import authentication from 'services/api/authentication';
let middlewareAdded = false;
export function authenticate(token, refreshToken) { // TODO: this action, probably, belongs to components/auth
const jwt = getJWTPayload(token);
return (dispatch, getState) => {
refreshToken = refreshToken || getState().user.refreshToken;
if (jwt.exp < Date.now() / 1000) {
return authentication.refreshToken(refreshToken)
.then((resp) => dispatch(authenticate(resp.access_token)))
.catch(() => dispatch(logout()));
if (!middlewareAdded) {
request.addMiddleware(tokenCheckMiddleware(dispatch, getState));
request.addMiddleware(tokenApplyMiddleware(dispatch, getState));
middlewareAdded = true;
}
request.setAuthToken(token);
refreshToken = refreshToken || getState().user.refreshToken;
dispatch(updateUser({
token,
refreshToken
}));
return dispatch(fetchUserData()).then((resp) => {
dispatch(updateUser({
isGuest: false,
token,
refreshToken
isGuest: false
}));
return resp;
});
};
}
import authentication from 'services/api/authentication';
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('.');

View File

@ -26,43 +26,55 @@ function buildQuery(data = {}) {
;
}
function doFetch(...args) {
// NOTE: we are wrapping fetch, because it is returning
// Promise instance that can not be pollyfilled with Promise.prototype.finally
return new Promise((resolve, reject) => fetch(...args).then(resolve, reject));
}
let authToken;
const middlewares = [];
const checkStatus = (resp) => Promise[resp.status >= 200 && resp.status < 300 ? 'resolve' : 'reject'](resp);
const toJSON = (resp) => resp.json();
const rejectWithJSON = (resp) => toJSON(resp).then((resp) => {throw resp;});
const handleResponse = (resp) => Promise[resp.success || typeof resp.success === 'undefined' ? 'resolve' : 'reject'](resp);
const handleResponseSuccess = (resp) => Promise[resp.success || typeof resp.success === 'undefined' ? 'resolve' : 'reject'](resp);
const getDefaultHeaders = () => {
const header = {Accept: 'application/json'};
function doFetch(url, options = {}) {
// NOTE: we are wrapping fetch, because it is returning
// Promise instance that can not be pollyfilled with Promise.prototype.finally
if (authToken) {
header.Authorization = `Bearer ${authToken}`;
}
options.headers = options.headers || {};
options.headers.Accept = 'application/json';
return header;
};
return runMiddlewares('before', {url, options})
.then(({url, options}) => fetch(url, options))
.then(checkStatus)
.then(toJSON, rejectWithJSON)
.then(handleResponseSuccess)
.then((resp) => runMiddlewares('then', resp))
.catch((resp) => runMiddlewares('catch', resp, () => doFetch(url, options)))
;
}
/**
* @param {string} action - the name of middleware's hook (before|then|catch)
* @param {Object} data - the initial data to pass through middlewares chain
* @param {Function} restart - a function to restart current request (for `catch` hook)
*
* @return {Promise}
*/
function runMiddlewares(action, data, restart) {
return middlewares
.filter((middleware) => middleware[action])
.reduce(
(promise, middleware) => promise.then((resp) => middleware[action](resp, restart)),
Promise[action === 'catch' ? 'reject' : 'resolve'](data)
);
}
export default {
post(url, data) {
return doFetch(url, {
method: 'POST',
headers: {
...getDefaultHeaders(),
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: buildQuery(data)
})
.then(checkStatus)
.then(toJSON, rejectWithJSON)
.then(handleResponse)
;
});
},
get(url, data) {
@ -71,18 +83,14 @@ export default {
url += separator + buildQuery(data);
}
return doFetch(url, {
headers: getDefaultHeaders()
})
.then(checkStatus)
.then(toJSON, rejectWithJSON)
.then(handleResponse)
;
return doFetch(url);
},
buildQuery,
setAuthToken(tkn) {
authToken = tkn;
addMiddleware(middleware) {
if (!middlewares.find((mdware) => mdware === middleware)) {
middlewares.push(middleware);
}
}
};