From 36c5f3264b63051a9807c1d372a2a909bfe8f751 Mon Sep 17 00:00:00 2001 From: Maximiliano Redigonda Date: Thu, 2 Jun 2022 11:39:31 -0300 Subject: [PATCH] [DEV-143] Handle expired sessions (#1192) --- README.md | 11 ++--- client/package.json | 4 +- .../actions/__mocks__/session-actions-mock.js | 2 +- .../actions/__tests__/session-actions-test.js | 8 +-- client/src/actions/session-actions.js | 2 +- .../app-components/session-expired-modal.js | 19 +++++++ client/src/data/languages/en.js | 2 + client/src/index.js | 2 +- client/src/lib-app/api-call.js | 49 +++++++++++-------- client/src/lib-app/expired-session-utils.js | 38 ++++++++++++++ server/models/Session.php | 2 + 11 files changed, 102 insertions(+), 37 deletions(-) create mode 100644 client/src/app-components/session-expired-modal.js create mode 100644 client/src/lib-app/expired-session-utils.js diff --git a/README.md b/README.md index a7b9f67e..8121aaff 100644 --- a/README.md +++ b/README.md @@ -13,18 +13,15 @@ Please, visit our website for more information: [http://www.opensupports.com/](h Here is a guide of how to set up the development environment in OpenSupports. ### Getting up and running FRONT-END (client folder) -1. Update: `sudo apt-get update` +1. Update: `sudo apt update` 2. Clone this repo: `git clone https://github.com/opensupports/opensupports.git` -3. Install node 4.x version: - - `sudo apt-get install curl` - - `curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -` - - `sudo apt-get install -y nodejs` -4. Install npm: `sudo apt-get install npm` +3. Install `nvm`: https://github.com/nvm-sh/nvm +4. Use node version 11.15.0: `nvm install 11` followed by `nvm use 11` 5. Go to client: `cd opensupports/client` 6. Install dependencies: `npm install` 7. Rebuild node-sass: `npm rebuild node-sass` 8. Run: `npm start` (PHP server api it must be running at :8080) -10. Go to the main app: `http://localhost:3000/app` or to the component demo `http://localhost:3000/demo` +10. Go to the main app: `http://localhost:3000/app` 11. Your browser will automatically be opened and directed to the browser-sync proxy address. 12. Use `npm start-fixtures` to enable fixtures and not require php server to be running. diff --git a/client/package.json b/client/package.json index 2a7f0b91..20467599 100644 --- a/client/package.json +++ b/client/package.json @@ -9,8 +9,8 @@ }, "private": false, "engines": { - "node": "^0.12.x", - "npm": "^2.1.x" + "node": "^11.15.x", + "npm": "^6.7.x" }, "scripts": { "start": "webpack-dev-server --display-reasons --display-error-details --history-api-fallback --progress --colors", diff --git a/client/src/actions/__mocks__/session-actions-mock.js b/client/src/actions/__mocks__/session-actions-mock.js index a34e9c24..6022cbbd 100644 --- a/client/src/actions/__mocks__/session-actions-mock.js +++ b/client/src/actions/__mocks__/session-actions-mock.js @@ -1,5 +1,5 @@ export default { login: stub(), logout: stub(), - initSession: stub() + checkSession: stub() }; \ No newline at end of file diff --git a/client/src/actions/__tests__/session-actions-test.js b/client/src/actions/__tests__/session-actions-test.js index 5b42fabb..92a1f82c 100644 --- a/client/src/actions/__tests__/session-actions-test.js +++ b/client/src/actions/__tests__/session-actions-test.js @@ -88,7 +88,7 @@ // }); // }); -// describe('initSession action', function () { +// describe('checkSession action', function () { // beforeEach(function () { // APICallMock.call.returns({ // then: function (resolve) { @@ -125,7 +125,7 @@ // } // }); -// expect(SessionActions.initSession().type).to.equal('CHECK_SESSION'); +// expect(SessionActions.checkSession().type).to.equal('CHECK_SESSION'); // expect(storeMock.dispatch).to.have.been.calledWith({type: 'SESSION_CHECKED'}); // expect(APICallMock.call).to.have.been.calledWith({ // path: '/user/check-session', @@ -136,7 +136,7 @@ // it('should return CHECK_SESSION and dispatch LOGOUT_FULFILLED if session is not active and no remember data', function () { // sessionStoreMock.isRememberDataExpired.returns(true); -// expect(SessionActions.initSession().type).to.equal('CHECK_SESSION'); +// expect(SessionActions.checkSession().type).to.equal('CHECK_SESSION'); // expect(storeMock.dispatch).to.have.been.calledWith({type: 'LOGOUT_FULFILLED'}); // expect(APICallMock.call).to.have.been.calledWith({ // path: '/user/check-session', @@ -147,7 +147,7 @@ // it('should return CHECK_SESSION and dispatch LOGIN_AUTO if session is not active but remember data exists', function () { // sessionStoreMock.isRememberDataExpired.returns(false); -// expect(SessionActions.initSession().type).to.equal('CHECK_SESSION'); +// expect(SessionActions.checkSession().type).to.equal('CHECK_SESSION'); // expect(storeMock.dispatch).to.not.have.been.calledWith({type: 'LOGOUT_FULFILLED'}); // expect(APICallMock.call).to.have.been.calledWith({ // path: '/user/check-session', diff --git a/client/src/actions/session-actions.js b/client/src/actions/session-actions.js index 9bec3100..3e862cf3 100644 --- a/client/src/actions/session-actions.js +++ b/client/src/actions/session-actions.js @@ -101,7 +101,7 @@ export default { }; }, - initSession() { + checkSession() { return { type: 'CHECK_SESSION', payload: new Promise((resolve, reject) => { diff --git a/client/src/app-components/session-expired-modal.js b/client/src/app-components/session-expired-modal.js new file mode 100644 index 00000000..e266a319 --- /dev/null +++ b/client/src/app-components/session-expired-modal.js @@ -0,0 +1,19 @@ +import React from "react"; +import _ from "lodash"; + +import i18n from "lib-app/i18n"; + +import Header from "core-components/header"; + +class SessionExpiredModal extends React.Component { + render() { + return ( +
+ ); + } +} + +export default SessionExpiredModal; diff --git a/client/src/data/languages/en.js b/client/src/data/languages/en.js index 65195a0f..48f01041 100644 --- a/client/src/data/languages/en.js +++ b/client/src/data/languages/en.js @@ -250,6 +250,7 @@ export default { 'USER_UNLOGGED_IN': 'This user has never logged in before', 'RESEND_STAFF_INVITATION_SUCCESS': 'The invitation was sent successfully', 'RESEND_STAFF_INVITATION_FAIL': 'The invitation could not be sent', + 'SESSION_EXPIRED': 'Session expired', //ACTIVITIES 'ACTIVITY_COMMENT': 'commented ticket', @@ -372,6 +373,7 @@ export default { 'INVITE_USER_VIEW_DESCRIPTION': 'Here you can invite an user to join our support system, he will just need to provide his password to create a new user.', 'INVITE_STAFF_DESCRIPTION': 'Here you can invite staff members to your teams.', 'TICKETS_INFORMATION' : 'Tickets from departments you don’t have assigned won’t be visible.', + 'SESSION_EXPIRED_DESCRIPTION': 'Your session has timed out. Please log in again.', //ERRORS 'EMAIL_OR_PASSWORD': 'Email or password invalid', diff --git a/client/src/index.js b/client/src/index.js index 33be2781..157787b2 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -52,4 +52,4 @@ history.listen(() => { store.dispatch(ConfigActions.checkInstallation()); store.dispatch(ConfigActions.init()); -store.dispatch(SessionActions.initSession()); +store.dispatch(SessionActions.checkSession()); diff --git a/client/src/lib-app/api-call.js b/client/src/lib-app/api-call.js index c440b5f2..2b9792ae 100644 --- a/client/src/lib-app/api-call.js +++ b/client/src/lib-app/api-call.js @@ -1,29 +1,34 @@ -const _ = require('lodash'); -const APIUtils = require('lib-core/APIUtils').default; -const SessionStore = require('lib-app/session-store').default; +const _ = require("lodash"); +const APIUtils = require("lib-core/APIUtils").default; +const SessionStore = require("lib-app/session-store").default; +import expiredSessionUtils from "./expired-session-utils"; -function processData (data, dataAsForm = false) { +function processData(data, dataAsForm = false) { let newData; - if(dataAsForm) { + if (dataAsForm) { newData = new FormData(); _.each(data, (value, key) => { newData.append(key, value); }); - newData.append('csrf_token', SessionStore.getSessionData().token); - newData.append('csrf_userid', SessionStore.getSessionData().userId); + newData.append("csrf_token", SessionStore.getSessionData().token); + newData.append("csrf_userid", SessionStore.getSessionData().userId); } else { - newData = _.extend({ - csrf_token: SessionStore.getSessionData().token, - csrf_userid: SessionStore.getSessionData().userId - }, data) + newData = _.extend( + { + csrf_token: SessionStore.getSessionData().token, + csrf_userid: SessionStore.getSessionData().userId, + }, + data + ); } return newData; } + const randomString = (length) => { var result = ""; var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; @@ -33,9 +38,8 @@ const randomString = (length) => { } return result; }; - -module.exports = { - call: function ({path, data, plain, dataAsForm}) { +export default { + call: function ({ path, data, plain, dataAsForm }) { const callId = randomString(3); const boldStyle = 'font-weight: bold;'; const normalStyle = 'font-weight: normal;'; @@ -48,26 +52,29 @@ module.exports = { if (showLogs) { console.log(`▶ Result %c${path}%c [${callId}]: `, boldStyle, normalStyle, result); } - - if (plain || result.status === 'success') { + const { status, message } = result; + if (plain || status === "success") { resolve(result); } else if (reject) { + if (status === "fail" && message === "NO_PERMISSION") { + expiredSessionUtils.checkSessionOrLogOut(); + } reject(result); } }) .catch(function (result) { - console.log('INVALID REQUEST to: ' + path); + console.log("INVALID REQUEST to: " + path); console.log(result); reject({ - status: 'fail', - message: 'Internal server error' + status: "fail", + message: "Internal server error", }); }); }); }, getFileLink(filePath) { - return apiRoot + '/system/download?file=' + filePath; + return apiRoot + "/system/download?file=" + filePath; }, getAPIUrl() { @@ -76,5 +83,5 @@ module.exports = { getURL() { return root; - } + }, }; diff --git a/client/src/lib-app/expired-session-utils.js b/client/src/lib-app/expired-session-utils.js new file mode 100644 index 00000000..0ec529ee --- /dev/null +++ b/client/src/lib-app/expired-session-utils.js @@ -0,0 +1,38 @@ +const _ = require("lodash"); +const APIUtils = require("lib-core/APIUtils").default; +import SessionActions from "../actions/session-actions"; +import store from "app/store"; +import ModalContainer from "app-components/modal-container"; +import SessionExpiredModal from "../app-components/session-expired-modal"; + +const OPEN_MODAL_OPTIONS = { + outsideClick: true, + closeButton: { + showCloseButton: false, + }, +}; + +const LOG_OUT_DELAY = 750; +const MODAL_DISAPPEAR_DELAY = 3000; + +function onSessionExpired(result) { + if (result.status === "success" && result.data.sessionActive === false) { + ModalContainer.openModal(, OPEN_MODAL_OPTIONS); + setTimeout(() => { + store.dispatch(SessionActions.checkSession()); + }, LOG_OUT_DELAY); + setTimeout(() => { + ModalContainer.closeModal(); + }, MODAL_DISAPPEAR_DELAY); + } +} + +export default { + checkSessionOrLogOut() { + APIUtils.post(apiRoot + "/user/check-session", {}, {}) + .then(onSessionExpired) + .catch((err) => + console.error("An error ocurred when checking session: ", err) + ); + }, +}; diff --git a/server/models/Session.php b/server/models/Session.php index 44803ded..61285e14 100755 --- a/server/models/Session.php +++ b/server/models/Session.php @@ -2,6 +2,7 @@ ini_set('session.cookie_httponly', 1); ini_set('session.cookie_secure', getenv('IS_DOCKER') ? 0 : 1); +ini_set('session.gc_maxlifetime', 3600 * 24 * 30); class Session { use SingletonTrait; @@ -13,6 +14,7 @@ class Session { } public function initSession() { + session_set_cookie_params(3600 * 24 * 30); session_cache_limiter(false); session_start(); }