From 7c5376bafb6657934d4eeaef5c2fc3765718bf55 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Jan 25 2018 08:12:14 +0000 Subject: Make sure the user session does not expire Fixes #530 --- diff --git a/hubs/static/client/app/components/Heartbeat.js b/hubs/static/client/app/components/Heartbeat.js new file mode 100644 index 0000000..1e0c5be --- /dev/null +++ b/hubs/static/client/app/components/Heartbeat.js @@ -0,0 +1,58 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { + sendHeartbeat, +} from "../core/actions/heartbeat"; + + +class Heartbeat extends React.PureComponent { + + constructor(props) { + super(props); + this.timeout = null; + this.interval = 60 * 1000; + this.ping = this.ping.bind(this); + } + + componentWillMount() { + if (this.timeout !== null) { + clearTimeout(this.timeout); + } + this.ping(); + } + + componentWillUnmount() { + if (this.timeout === null) { + return; + } + clearTimeout(this.timeout); + this.timeout = null; + } + + ping() { + this.props.sendHeartbeat(); + this.timeout = setTimeout(this.ping, this.interval); + } + + render() { + return null; + } +} +Heartbeat.propTypes = { + connected: PropTypes.bool, +} +Heartbeat.defaultProps = { + connected: true, +} + + +const mapStateToProps = (state) => { + return { + connected: state.ui.heartbeat === "connected", + } +}; + +export default connect(mapStateToProps, { + sendHeartbeat, +})(Heartbeat); diff --git a/hubs/static/client/app/components/PageStructure.js b/hubs/static/client/app/components/PageStructure.js index 5210e8f..5279182 100644 --- a/hubs/static/client/app/components/PageStructure.js +++ b/hubs/static/client/app/components/PageStructure.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import FlashMessages from './FlashMessages'; +import Heartbeat from './Heartbeat'; import LeftMenu from './LeftMenu'; @@ -28,6 +29,8 @@ export default class PageStructure extends React.Component { + + ); } diff --git a/hubs/static/client/app/components/SSESource.js b/hubs/static/client/app/components/SSESource.js index dcab4be..1b41d19 100644 --- a/hubs/static/client/app/components/SSESource.js +++ b/hubs/static/client/app/components/SSESource.js @@ -18,7 +18,6 @@ class SSESource extends React.PureComponent { isEnabled: true, } this.source = null; - this.notifTimeout = null; this.setupSource = this.setupSource.bind(this); } diff --git a/hubs/static/client/app/core/actions/heartbeat.js b/hubs/static/client/app/core/actions/heartbeat.js new file mode 100644 index 0000000..87604d7 --- /dev/null +++ b/hubs/static/client/app/core/actions/heartbeat.js @@ -0,0 +1,36 @@ +import { backendCall } from "../utils"; +import { addFlashMessage } from './flashMessages'; + + +export const HEARTBEAT_SUCCESS = 'HEARTBEAT_SUCCESS' +export const HEARTBEAT_FAILURE = 'HEARTBEAT_FAILURE' + + +function heartbeatFailure(code) { + return { + type: HEARTBEAT_FAILURE, + code, + }; +} + + +export function sendHeartbeat() { + return (dispatch, getState) => { + const state = getState(); + const url = state.urls.heartbeat; + const heartbeatConnected = state.ui.heartbeat === "connected"; + return backendCall(url).then( + (response) => { + dispatch({ + type: HEARTBEAT_SUCCESS + }) + }, + (error) => { + if (error.httpStatus === 401 && heartbeatConnected) { + dispatch(addFlashMessage("Your session timed out, please reload the page", "error")); + } + return dispatch(heartbeatFailure(error.httpStatus)); + } + ); + } +} diff --git a/hubs/static/client/app/core/actions/widget.js b/hubs/static/client/app/core/actions/widget.js index 5af6e96..ed6dc11 100644 --- a/hubs/static/client/app/core/actions/widget.js +++ b/hubs/static/client/app/core/actions/widget.js @@ -176,7 +176,7 @@ export function moveWidget(position, oldIndex, newIndex) { return dispatch(fetchWidgets()); }, error => { - return dispatch(moveWidgetFailure(position, oldIndex, newIndex, e.message)); + return dispatch(moveWidgetFailure(position, oldIndex, newIndex, error.message)); }) } } diff --git a/hubs/static/client/app/core/reducers/heartbeat.js b/hubs/static/client/app/core/reducers/heartbeat.js new file mode 100644 index 0000000..f1623fa --- /dev/null +++ b/hubs/static/client/app/core/reducers/heartbeat.js @@ -0,0 +1,17 @@ +import { + HEARTBEAT_SUCCESS, + HEARTBEAT_FAILURE, +} from '../actions/heartbeat'; + + +export default function heartbeat(state="connected", action) { + switch (action.type) { + case HEARTBEAT_SUCCESS: + return "connected"; + case HEARTBEAT_FAILURE: + return "disconnected"; + default: + return state + } +} + diff --git a/hubs/static/client/app/core/reducers/index.js b/hubs/static/client/app/core/reducers/index.js index 981550a..d8e40f1 100644 --- a/hubs/static/client/app/core/reducers/index.js +++ b/hubs/static/client/app/core/reducers/index.js @@ -1,5 +1,6 @@ import { combineReducers } from 'redux'; import flashMessages from "./flashMessages"; +import heartbeat from "./heartbeat"; import sseReducer from "./sse"; import { hubReducer, @@ -35,6 +36,7 @@ const ui = combineReducers({ widgetConfigDialogOpen, hubConfigDialogOpen, hubConfigDialogLoading, + heartbeat, }); diff --git a/hubs/static/client/app/core/utils.js b/hubs/static/client/app/core/utils.js index 77148c8..09c0361 100644 --- a/hubs/static/client/app/core/utils.js +++ b/hubs/static/client/app/core/utils.js @@ -20,6 +20,7 @@ export function backendCall(url, fetchConfig) { resultPromise = resultPromise.then((content) => { if (isJson) { // It's an object describing the error. + content.httpStatus = response.status; throw content; } else { // It's already a text message. diff --git a/hubs/views/hub.py b/hubs/views/hub.py index 686c8ed..d19a256 100644 --- a/hubs/views/hub.py +++ b/hubs/views/hub.py @@ -31,6 +31,7 @@ def hub(hub_type, hub_name): "api_hub_config_suggest_users", hub_id=hub.id), "user": flask.url_for("api_user"), "allGroups": flask.url_for("groups"), + "heartbeat": flask.url_for("heartbeat"), } current_user = get_user_details() flash_messages = [ diff --git a/hubs/views/root.py b/hubs/views/root.py index 441372e..0caddee 100644 --- a/hubs/views/root.py +++ b/hubs/views/root.py @@ -29,6 +29,7 @@ def groups(): current_user = get_user_details() urls = { "allGroups": flask.url_for("groups"), + "heartbeat": flask.url_for("heartbeat"), } flash_messages = [ {"msg": msg[1], "type": msg[0]} for msg in @@ -76,6 +77,14 @@ def groups(): ) +@app.route('/ping') +def heartbeat(): + if authenticated(): + return flask.jsonify({"status": "OK"}) + else: + return flask.jsonify({"status": "ERROR"}), 401 + + @app.route('/login/', methods=('GET', 'POST')) @app.route('/login', methods=('GET', 'POST')) @OIDC.require_login diff --git a/hubs/views/user.py b/hubs/views/user.py index 5db0e49..8c8f53a 100644 --- a/hubs/views/user.py +++ b/hubs/views/user.py @@ -20,6 +20,7 @@ def stream(): "widgets": flask.url_for("api_hub_widgets", hub_id=stream.id), "availableWidgets": flask.url_for("api_widgets", hub_id=stream.id), "allGroups": flask.url_for("groups"), + "heartbeat": flask.url_for("heartbeat"), } flash_messages = [ {"msg": msg[1], "type": msg[0]} for msg in