#186: Decouple user middlewares from user actions

This commit is contained in:
SleepWalker 2016-08-07 22:17:58 +03:00
parent 322325d4ad
commit 32496e99d9
4 changed files with 122 additions and 119 deletions

View File

@ -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');
}
}

View File

@ -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();

View File

@ -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;
}
};
}

View File

@ -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');
}
}