diff --git a/app/__init__.py b/app/__init__.py index 7c5879a30..e73da2046 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -321,7 +321,7 @@ def make_session_permanent(): """ Make sessions permanent. By permanent, we mean "admin app sets when it expires". Normally the cookie would expire whenever you close the browser. With this, the session expiry is set in `config['PERMANENT_SESSION_LIFETIME']` - (20 hours) and is refreshed after every request. IE: you will be logged out after twenty hours of inactivity. + (30 min) and is refreshed after every request. IE: you will be logged out after thirty minutes of inactivity. We don't _need_ to set this every request (it's saved within the cookie itself under the `_permanent` flag), only when you first log in/sign up/get invited/etc, but we do it just to be safe. For more reading, check here: diff --git a/app/assets/javascripts/timeoutPopup.js b/app/assets/javascripts/timeoutPopup.js new file mode 100644 index 000000000..ed33cf903 --- /dev/null +++ b/app/assets/javascripts/timeoutPopup.js @@ -0,0 +1,65 @@ +window.GOVUK = window.GOVUK || {}; +window.GOVUK.Modules = window.GOVUK.Modules || {}; +window.GOVUK.Modules.TimeoutPopup = window.GOVUK.Modules.TimeoutPopup || {}; + +(function(global) { + "use strict"; + + const sessionTimer = document.getElementById("sessionTimer"); + let intervalId = null; + + function checkTimer(timeTillSessionEnd) { + var now = new Date().getTime(); + var difference = timeTillSessionEnd - now; + var minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)); + var seconds = Math.floor((difference % (1000 * 60)) / 1000); + document.getElementById("timeLeft").innerHTML = + minutes + "m " + seconds + "s"; + showTimer(); + document.getElementById("logOutTimer").addEventListener("click", signoutUser); + document.getElementById("extendSessionTimer").addEventListener("click", extendSession); + if (difference < 0) { + clearInterval(intervalId); + intervalId = null; + closeTimer(); + expireUserSession(); + } + } + + function expireUserSession() { + var signOutLink = '/sign-out?next=' + window.location.pathname; + window.location.href = signOutLink; + + } + + function signoutUser() { + window.location.href = '/sign-out'; + } + + function extendSession() { + window.location.reload(); + } + + function showTimer() { + sessionTimer.showModal(); + } + + function closeTimer() { + sessionTimer.close(); + } + + function setSessionTimer() { + var timeTillSessionEnd = new Date().getTime() + (5 * 60 * 1000); + intervalId = setInterval(checkTimer, 1000, timeTillSessionEnd); + } + + if (document.getElementById("timeLeft") !== null) { + setTimeout(setSessionTimer, 25 * 60 * 1000); + } + + global.GOVUK.Modules.TimeoutPopup.checkTimer = checkTimer; + global.GOVUK.Modules.TimeoutPopup.expireUserSession = expireUserSession; + global.GOVUK.Modules.TimeoutPopup.signoutUser = signoutUser; + global.GOVUK.Modules.TimeoutPopup.extendSession = extendSession; + global.GOVUK.Modules.TimeoutPopup.showTimer = showTimer; + global.GOVUK.Modules.TimeoutPopup.closeTimer = closeTimer; +})(window); diff --git a/app/config.py b/app/config.py index 6215a744d..f424076fa 100644 --- a/app/config.py +++ b/app/config.py @@ -54,7 +54,7 @@ class Config(object): EMAIL_EXPIRY_SECONDS = 3600 # 1 hour INVITATION_EXPIRY_SECONDS = 3600 * 24 * 2 # 2 days - also set on api EMAIL_2FA_EXPIRY_SECONDS = 1800 # 30 Minutes - PERMANENT_SESSION_LIFETIME = 20 * 60 * 60 # 20 hours + PERMANENT_SESSION_LIFETIME = 1800 # 30 Minutes SEND_FILE_MAX_AGE_DEFAULT = 365 * 24 * 60 * 60 # 1 year REPLY_TO_EMAIL_ADDRESS_VALIDATION_TIMEOUT = 45 ACTIVITY_STATS_LIMIT_DAYS = 7 diff --git a/app/templates/admin_template.html b/app/templates/admin_template.html index 5e3746262..e30602bfb 100644 --- a/app/templates/admin_template.html +++ b/app/templates/admin_template.html @@ -137,7 +137,6 @@ {% endblock %} {% block footer %} - {% if current_service and current_service.research_mode %} {% set meta_suffix = 'Built by the Technology Transformation Servicesresearch mode' %} @@ -216,8 +215,45 @@ "html": meta_suffix } }) }} + + {% if current_user.is_authenticated %} + {% block sessionUserWarning %} + + + + + Your session will end soon. + Please choose to extend your session or sign out. Your session will expire in 5 minutes or less. + + + You have been inactive for too long. + Your session will expire in . + + + + + + + {% endblock %} + {% endif %} + {% endblock %} + {% block bodyEnd %} {% block extra_javascripts %} {% endblock %} @@ -225,4 +261,7 @@ + {% endblock %} + + diff --git a/gulpfile.js b/gulpfile.js index 423e4fe85..f26ef6f39 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -125,6 +125,7 @@ const javascripts = () => { paths.src + 'javascripts/updateStatus.js', paths.src + 'javascripts/errorBanner.js', paths.src + 'javascripts/homepage.js', + paths.src + 'javascripts/timeoutPopup.js', paths.src + 'javascripts/main.js', ]) .pipe(plugins.prettyerror()) diff --git a/tests/javascripts/jest.config.js b/tests/javascripts/jest.config.js index 6b649f57f..0e3dc0128 100644 --- a/tests/javascripts/jest.config.js +++ b/tests/javascripts/jest.config.js @@ -12,6 +12,6 @@ module.exports = { setupFiles: ['./support/setup.js'], testEnvironment: 'jsdom', testEnvironmentOptions: { - url: 'https://www.notifications.service.gov.uk', + url: 'https://beta.notify.gov', }, }; diff --git a/tests/javascripts/support/polyfills.js b/tests/javascripts/support/polyfills.js index 8e2170704..1549cbf8f 100644 --- a/tests/javascripts/support/polyfills.js +++ b/tests/javascripts/support/polyfills.js @@ -2,9 +2,9 @@ let _location = { reload: jest.fn(), - hostname: "www.notifications.service.gov.uk", + hostname: "beta.notify.gov", assign: jest.fn(), - href: "https://www.notifications.service.gov.uk", + href: "https://beta.notify.gov", } // JSDOM provides a read-only window.location, which does not allow for diff --git a/tests/javascripts/timeoutPopup.test.js b/tests/javascripts/timeoutPopup.test.js new file mode 100644 index 000000000..afc8a2687 --- /dev/null +++ b/tests/javascripts/timeoutPopup.test.js @@ -0,0 +1,131 @@ +beforeAll(() => { + jest.spyOn(global, 'setTimeout'); + + document.body.innerHTML = ` + + + + + Your session will end soon. + Please choose to extend your session or sign out. Your session will expire in 5 minutes or less. + + + You have been inactive for too long. + Your session will expire in . + + + + + + + ` + + const sessionTimerModule = require('../../app/assets/javascripts/timeoutPopup.js'); + window.GOVUK.modules.start(); +}); + +afterAll(() => { + document.body.innerHTML = ''; +}); + + +describe('When the session timer module is loaded', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useFakeTimers(); + }); + + test('everything initializes properly', () => { + const sessionTimer = document.getElementById("sessionTimer"); + sessionTimer.showModal = jest.fn(); + sessionTimer.close = jest.fn(); + + jest.runAllTimers(); + }); + +}); + + +describe('The session timer ', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useFakeTimers(); + }); + + test('signoutUser method logs the user out', () => { + const signoutUserMethod = window.GOVUK.Modules.TimeoutPopup.signoutUser; + + expect(window.location.href).toEqual(expect.not.stringContaining('/sign-out')); + + signoutUserMethod(); + + expect(window.location.href).toEqual(expect.stringContaining('/sign-out')); + }); + + test('expireUserSession method logs the user out with next query parameter', () => { + const expireUserSessionMethod = window.GOVUK.Modules.TimeoutPopup.expireUserSession; + + expect(window.location.href).toEqual(expect.not.stringContaining('/sign-out?next=')); + + expireUserSessionMethod(); + + expect(window.location.href).toEqual(expect.stringContaining('/sign-out?next=')); + }); + + test('extendSession method reloads the page', () => { + const windowReload = jest.spyOn(window.location, 'reload'); + const extendSessionMethod = window.GOVUK.Modules.TimeoutPopup.extendSession; + + extendSessionMethod(); + + expect(windowReload).toHaveBeenCalled(); + }); + + test('showTimer method shows the session timer modal', () => { + const sessionTimer = document.getElementById("sessionTimer"); + sessionTimer.showModal = jest.fn(); + + const showTimerMock = jest.spyOn(sessionTimer, 'showModal'); + + window.GOVUK.Modules.TimeoutPopup.showTimer(); + + expect(showTimerMock).toHaveBeenCalled(); + }); + + test('closeTimer method closes the session timer modal', () => { + const sessionTimer = document.getElementById("sessionTimer"); + sessionTimer.close = jest.fn(); + + const closeTimerMock = jest.spyOn(sessionTimer, 'close'); + + window.GOVUK.Modules.TimeoutPopup.closeTimer(); + + expect(closeTimerMock).toHaveBeenCalled(); + }); + + test('checkTimer is called', () => { + const checkTimerMock = jest.spyOn(window.GOVUK.Modules.TimeoutPopup, "checkTimer"); + window.GOVUK.Modules.TimeoutPopup.checkTimer(); + expect(checkTimerMock).toHaveBeenCalled(); + }); +});
You have been inactive for too long. + Your session will expire in . +