Improve the network saga. Try to avoid unnecessary token refresh
authorymh <ymh.work@gmail.com>
Tue, 27 Jun 2017 11:38:26 +0200
changeset 97 69eaef18b01b
parent 96 b58463d7dc8e
child 98 2e939d9cf193
Improve the network saga. Try to avoid unnecessary token refresh
client/.env
client/package.json
client/src/api/APIClient.js
client/src/config.js
client/src/sagas/networkSaga.js
client/yarn.lock
src/irinotes/settings.py
--- 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/.*$'