# HG changeset patch # User ymh # Date 1498556306 -7200 # Node ID 69eaef18b01b267b3492739f2a05c18e0bb9e822 # Parent b58463d7dc8e0b78f923cfb7c574871205b62f0c Improve the network saga. Try to avoid unnecessary token refresh diff -r b58463d7dc8e -r 69eaef18b01b client/.env --- 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 diff -r b58463d7dc8e -r 69eaef18b01b client/package.json --- 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", diff -r b58463d7dc8e -r 69eaef18b01b client/src/api/APIClient.js --- 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) => { diff -r b58463d7dc8e -r 69eaef18b01b client/src/config.js --- 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, } diff -r b58463d7dc8e -r 69eaef18b01b client/src/sagas/networkSaga.js --- 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) ]); diff -r b58463d7dc8e -r 69eaef18b01b client/yarn.lock --- 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" diff -r b58463d7dc8e -r 69eaef18b01b src/irinotes/settings.py --- 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/.*$'