Improve the network saga. Try to avoid unnecessary token refresh
--- a/client/.env Tue Jun 27 10:54:04 2017 +0200
+++ b/client/.env Tue Jun 27 11:38:26 2017 +0200
@@ -1,4 +1,4 @@
REACT_APP_API_ROOT_URL = http://localhost:8000
REACT_APP_BASENAME =
-REACT_APP_NETWORK_STATUS_INTERVAL = 2000
+REACT_APP_NETWORK_STATUS_INTERVAL = 20000
REACT_APP_NETWORK_STATUS_TIMEOUT = 2000
--- a/client/package.json Tue Jun 27 10:54:04 2017 +0200
+++ b/client/package.json Tue Jun 27 11:38:26 2017 +0200
@@ -5,6 +5,7 @@
"homepage": ".",
"dependencies": {
"immutable": "^3.8.1",
+ "jwt-decode": "^2.2.0",
"localforage": "^1.5.0",
"lodash": "^4.17.4",
"moment": "^2.18.1",
--- a/client/src/api/APIClient.js Tue Jun 27 10:54:04 2017 +0200
+++ b/client/src/api/APIClient.js Tue Jun 27 11:38:26 2017 +0200
@@ -10,7 +10,12 @@
createRequest = (method, uri, data, headers) => {
headers = headers || new Headers();
- headers.append("Content-Type", "application/json");
+ if(method !== 'HEAD') {
+ headers.append("Content-Type", "application/json");
+ } else {
+ headers.append("Content-Type", "text/plain");
+ }
+
var options = {
method: method,
@@ -46,7 +51,6 @@
}
request = (method, uri, data) => {
- console.log(method + ' ' + uri);
var req = this.hasToken() ? this.createAuthorizedRequest(method, uri, data) : this.createRequest(method, uri, data);
return this.fetch(req, { credentials: 'include' });
}
@@ -72,12 +76,18 @@
if(response.status === 204) {
resJsonPromise = Promise.resolve({});
} else {
- resJsonPromise = response.json();
+ resJsonPromise = response.text().then(data => {
+ if(data.length > 0) {
+ return JSON.parse(data);
+ } else {
+ return {};
+ }
+ });
}
- return resJsonPromise.then((data) => resolve(data));
+ return resJsonPromise.then(data => resolve(data));
} else {
- return response.json().then((data) => reject(data));
+ return response.json().then(data => reject(data));
}
})
.catch((error) => {
--- a/client/src/config.js Tue Jun 27 10:54:04 2017 +0200
+++ b/client/src/config.js Tue Jun 27 11:38:26 2017 +0200
@@ -4,5 +4,5 @@
apiRootUrl: process.env.REACT_APP_API_ROOT_URL || 'http://localhost:8000',
basename: process.env.REACT_APP_BASENAME || '',
networkStatusTimeout: parseInt(process.env.REACT_APP_NETWORK_STATUS_TIMEOUT, 10) || 2000,
- networkStatusInterval: parseInt(process.env.REACT_APP_NETWORK_STATUS_INTERVAL, 10) || 2000,
+ networkStatusInterval: parseInt(process.env.REACT_APP_NETWORK_STATUS_INTERVAL, 10) || 20000,
}
--- a/client/src/sagas/networkSaga.js Tue Jun 27 10:54:04 2017 +0200
+++ b/client/src/sagas/networkSaga.js Tue Jun 27 11:38:26 2017 +0200
@@ -1,6 +1,9 @@
import * as types from '../constants/actionTypes';
import { all, call, fork, race, take, cancelled, put, select } from 'redux-saga/effects'
import config from '../config';
+import * as persistConstants from 'redux-persist/constants';
+import jwt_decode from 'jwt-decode';
+import moment from 'moment';
// Utility function to delay effects
function delay(millis) {
@@ -11,39 +14,51 @@
}
function pingServer(client, token) {
- if(token) {
- const timeout = new Promise((resolve, reject) => {
- setTimeout(reject, config.networkStatusTimeout, 'request timed out');
- });
+ const decodedToken = jwt_decode(token);
+ const currentTs = moment.now()/1000;
+
+ const timeout = new Promise((resolve, reject) => {
+ setTimeout(reject, config.networkStatusTimeout, 'request timed out');
+ });
+
+ if((decodedToken.exp-currentTs) < 300) {
return Promise
.race([timeout, client.post('/api/auth/refresh/', { token })]);
} else {
- return Promise.reject({ error: 'No token in the store'})
+ // We do a GET because a HEAD generate a preflight CORS OPTION request. The GET does not.
+ return Promise
+ .race([timeout, client.get('/api/auth/user/')]);
}
}
-// Fetch data every 20 seconds
function* pollData(context) {
+ const token = yield select(state => state.token);
+ // No token : we wait for a login
+ if(!token) {
+ yield take(types.AUTH_LOGIN_SUCCESS);
+ }
try {
- yield call(delay, config.networkStatusInterval);
- const token = yield select(state => state.token);
const res = yield pingServer(context.client, token);
yield call(context.callback, true);
- yield put({
- type: types.AUTH_STORE_TOKEN_ASYNC,
- token: res.token,
- });
+ if(res.token) {
+ yield put({
+ type: types.AUTH_STORE_TOKEN_ASYNC,
+ token: res.token,
+ });
+ }
} catch (error) {
yield call(context.callback, false);
- // if the error is that there is no token, then we know we have to wait for a login
- if(error.error && error.error === 'No token in the store') {
- yield take(types.AUTH_LOGIN_SUCCESS);
- } else if (error.non_field_errors &&
+ //TODO: This is ugly...
+ if ((error.non_field_errors &&
error.non_field_errors &&
error.non_field_errors.length &&
error.non_field_errors.length > 0 &&
( error.non_field_errors[0] === 'Signature has expired.' ||
- error.non_field_errors[0] === 'Refresh has expired.' )
+ error.non_field_errors[0] === 'Refresh has expired.' )) ||
+ (error.detail && (
+ error.detail === 'Signature has expired.' ||
+ error.detail=== 'Refresh has expired.'
+ ))
) {
yield put({
type: types.AUTH_DEAUTHENTICATE
@@ -55,12 +70,21 @@
// if there is a token : this was a LOGIN, set status to ok
// if there is no token : this was a LOGOUT, set status to ko and wait for login
const token = yield select(state => state.token);
- if(token) {
- yield call(context.callback, true);
- } else {
- yield call(context.callback, false);
- yield take(types.AUTH_LOGIN_SUCCESS);
- }
+ yield call(context.callback, Boolean(token));
+ }
+ }
+}
+
+function* callDelay(context) {
+ try {
+ yield call(delay, config.networkStatusInterval);
+ } finally {
+ if (yield cancelled()) {
+ // pollDate cancelled
+ // if there is a token : this was a LOGIN, set status to ok
+ // if there is no token : this was a LOGOUT, set status to ko and wait for login
+ const token = yield select(state => state.token);
+ yield call(context.callback, Boolean(token));
}
}
}
@@ -68,9 +92,13 @@
// Wait for successful response, then fire another request
// Cancel polling if user logs out or log in
function* watchPollData(context) {
+
+ //wait for the state to be rehydrated
+ yield take(persistConstants.REHYDRATE);
+
while (true) {
yield race([
- call(pollData, context),
+ all([call(pollData, context), call(callDelay, context)]),
take(types.AUTH_LOGOUT),
take(types.AUTH_LOGIN_SUCCESS)
]);
--- a/client/yarn.lock Tue Jun 27 10:54:04 2017 +0200
+++ b/client/yarn.lock Tue Jun 27 11:38:26 2017 +0200
@@ -3827,6 +3827,10 @@
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1"
+jwt-decode@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
+
keycode@^2.1.2:
version "2.1.9"
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa"
--- a/src/irinotes/settings.py Tue Jun 27 10:54:04 2017 +0200
+++ b/src/irinotes/settings.py Tue Jun 27 11:38:26 2017 +0200
@@ -244,4 +244,6 @@
# CORS Headers
CORS_ORIGIN_ALLOW_ALL = True
+CORS_ALLOW_CREDENTIALS = True
+
CORS_URLS_REGEX = r'^/api/.*$'