From 7c1879ea5bdceca0647dbbf7dc4d002cff8aae5a Mon Sep 17 00:00:00 2001 From: Peter Hamilton Date: Mon, 20 May 2024 08:35:55 +0100 Subject: [PATCH] ER-988: Remove timeout modal functionality (#1151) * Remove timeout modal functionality Retire these locales: timeout.title timeout.heading timeout.body timeout.noscript * Configure using env vars and hide flash message --- .env.example | 7 +- README.md | 10 - app/assets/stylesheets/application.scss | 7 - app/assets/stylesheets/modal.scss | 66 --- app/controllers/application_controller.rb | 8 - app/controllers/timeout_controller.rb | 39 -- app/helpers/timeout_helper.rb | 26 - app/javascript/application.js | 23 +- app/javascript/controllers/application.js | 4 +- .../controllers/autocomplete_controller.js | 2 +- app/javascript/controllers/index.js | 6 +- app/javascript/timeout-warning.js | 456 ------------------ app/views/layouts/_flash.html.slim | 3 +- app/views/layouts/_timeout.html.slim | 36 -- app/views/layouts/application.html.slim | 2 - app/views/layouts/hero.html.slim | 1 - app/views/timeout/timeout_user.html.slim | 7 - config/application.rb | 9 +- config/initializers/devise.rb | 50 +- config/locales/en.yml | 11 - config/routes.rb | 41 +- config/sitemap.rb | 2 - docker-compose.qa.yml | 1 - spec/config_spec.rb | 8 - .../training/modules_controller_spec.rb | 30 -- spec/lib/seed_snippets_spec.rb | 2 +- spec/requests/static_spec.rb | 2 - spec/requests/timeout_warning_spec.rb | 60 --- spec/system/page_title_spec.rb | 1 - spec/system/timeout_modal_spec.rb | 51 -- 30 files changed, 43 insertions(+), 928 deletions(-) delete mode 100644 app/assets/stylesheets/modal.scss delete mode 100644 app/controllers/timeout_controller.rb delete mode 100644 app/helpers/timeout_helper.rb delete mode 100644 app/javascript/timeout-warning.js delete mode 100644 app/views/layouts/_timeout.html.slim delete mode 100644 app/views/timeout/timeout_user.html.slim delete mode 100644 spec/controllers/training/modules_controller_spec.rb delete mode 100644 spec/requests/timeout_warning_spec.rb delete mode 100644 spec/system/timeout_modal_spec.rb diff --git a/.env.example b/.env.example index 9768429be..2cb1ce360 100644 --- a/.env.example +++ b/.env.example @@ -49,12 +49,11 @@ POSTGRES_PASSWORD= # user research FEEDBACK_URL= -# logout -TIMEOUT_MINUTES=360 -TIMEOUT_WARNING_MINUTES=5 -# Time interval, in minutes, to unlock the account if :time is enabled as unlock_strategy +# Account unlock wait duration if :time is enabled UNLOCK_IN_MINUTES= +# Account session expiration aligned to GovOne by default +TIMEOUT_IN_MINUTES= # user acceptance tests BASE_URL= diff --git a/README.md b/README.md index 2a6acc222..bdf2295ec 100644 --- a/README.md +++ b/README.md @@ -312,16 +312,6 @@ User [service accounts](https://cloud.google.com/iam/docs/service-accounts) can - `gsutil -m cp -r "gs://eyfs-data-dashboard-live/eventsdata" "gs://eyfs-data-dashboard-live/useranswers" .` (export folders recursively) ---- - -## User experience - -Session timeout functionality: - -- default timeout period is 25 minutes -- default timeout warning appears after 5 minutes -- screen readers announce every time the timeout refreshes every 15 secs - --- ## Hotjar diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index c2488da9e..a071a6645 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -30,7 +30,6 @@ $light-blue: tint($department-for-education-websafe, 90%); @import 'interruption'; @import 'quote'; @import 'icons'; -@import 'modal'; @import 'autocomplete'; @import 'assets-page'; @@ -192,12 +191,6 @@ ul>li>ul>li { background: #f3f2f1; } -#js-timeout-warning:focus { - outline: 3px solid #ffdd00; - outline-offset: 0; - box-shadow: inset 0 0 0 2px; -} - .govuk-footer__copyright-logo { display: none; } diff --git a/app/assets/stylesheets/modal.scss b/app/assets/stylesheets/modal.scss deleted file mode 100644 index fe924d283..000000000 --- a/app/assets/stylesheets/modal.scss +++ /dev/null @@ -1,66 +0,0 @@ -/** - * - * Plugin has been in test by alphagov documentation and code links below - * - * Github repo - * https://github.com/hannalaakso/accessible-timeout-warning - * - * Documentation here - * https://github.com/alphagov/govuk-design-system-backlog/issues/104 - * - * Component which this has been copied and updated from - * https://github.com/hannalaakso/accessible-timeout-warning/tree/master/src/govuk/components/timeout-warning - * - * css file this belongs to - * https: //github.com/hannalaakso/accessible-timeout-warning/blob/master/src/govuk/components/timeout-warning/_timeout-warning.scss - * - */ - -.govuk-timeout-warning { - display: none; - @include govuk-responsive-padding(6); - // Setting width allows IE to center dialog vertically - width: 100%; - - // To do: allow this to be customised - max-width: 500px; - - .govuk-button { - margin: 0 govuk-spacing(2) govuk-spacing(3) 0; - - @include govuk-media-query($from: tablet) { - margin-bottom: govuk-spacing(0); - } - } - - &[open] { - display: block; - - $backdrop-colour: rgba(0, 0, 0, .8); - - // Backdrop styles for browsers with native support - &+.backdrop { - background: $backdrop-colour; - } - - // Backdrop styles for browsers using - // Keep these two backdrop styles separate - Safari didn't - // work when combined - &::backdrop { - background: $backdrop-colour; - } - } -} - -// Stop background scrolling while dialog open. -.govuk-timeout-warning-overlay { - overflow: hidden; -} - -.js-enabled .govuk-timeout-warning-fallback { - display: none; -} - -.tabular-numbers { - font-family: $govuk-font-family-tabular; -} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 836b51d8c..fad98a8af 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -8,7 +8,6 @@ class ApplicationController < ActionController::Base :prepare_cms helper_method :current_user, - :timeout_timer, :debug? default_form_builder ::FormBuilder @@ -58,13 +57,6 @@ def debug? Rails.application.debug? && !bot? end - def timeout_timer - timeout_controller = TimeoutController.new - timeout_controller.request = request - timeout_controller.response = response - timeout_controller.send(:ttl_to_timeout) - end - private # @return [Boolean] diff --git a/app/controllers/timeout_controller.rb b/app/controllers/timeout_controller.rb deleted file mode 100644 index 1baed6c4e..000000000 --- a/app/controllers/timeout_controller.rb +++ /dev/null @@ -1,39 +0,0 @@ -class TimeoutController < Devise::SessionsController - prepend_before_action :skip_timeout, only: %i[check] - - # @see TimeoutHelper - def timeout_user - sign_out current_user - end - - # @note clear etags to prevent caching - def check - response.headers['Etag'] = '' - render plain: ttl_to_timeout, status: :ok - end - - # tracks and therefore extends - def extend - check - end - -private - - # @see Rails.configuration.user_timeout_minutes - # @return [Integer] seconds until timeout - def ttl_to_timeout - return 0 if user_session.blank? - - Devise.timeout_in - (Time.zone.now.utc - last_request_time).to_i - end - - # @return [Integer] - def last_request_time - user_session['last_request_at'].presence || 0 - end - - # Prevent tracking of timeout timer value - def skip_timeout - request.env['devise.skip_trackable'] = true - end -end diff --git a/app/helpers/timeout_helper.rb b/app/helpers/timeout_helper.rb deleted file mode 100644 index d0d2cf0b8..000000000 --- a/app/helpers/timeout_helper.rb +++ /dev/null @@ -1,26 +0,0 @@ -module TimeoutHelper - # @return [Integer] minutes of inactivity until auto logout (default 25) - def timeout_duration - Rails.configuration.user_timeout_minutes - end - - # @return [Time] when user will be logged out - def timeout_time - Time.zone.now.advance(minutes: timeout_duration).to_fs(:time) - end - - # @return [Integer] minutes until timeout modal appears (default 5) - def timeout_warning - Rails.configuration.user_timeout_warning_minutes - end - - # @return [Integer] minutes modal should appears for (default 5) - def timeout_modal - Rails.configuration.user_timeout_modal_visible - end - - # @return [String] "true" or "false" - def timeout_active - current_user.present?.to_s - end -end diff --git a/app/javascript/application.js b/app/javascript/application.js index 18838d8a9..b4c68cfe1 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,9 +1,7 @@ -import "@hotwired/turbo-rails"; -import "@fortawesome/fontawesome-free/js/all"; +import '@hotwired/turbo-rails'; +import '@fortawesome/fontawesome-free/js/all'; -import TimeoutWarning from "./timeout-warning"; - -import "./controllers"; +import './controllers'; import { initAll } from "govuk-frontend"; @@ -16,19 +14,8 @@ function nodeListForEach (nodes, callback) { } } - /* -Govuk Accordion component suffers from lag without the turbo listener -*/ -document.addEventListener("turbo:load", function() { +document.addEventListener('turbo:load', function() { initAll(); - - /* - timeout functionality - */ - var $timeoutWarnings = document.querySelectorAll('[data-module="govuk-timeout-warning"]') - - nodeListForEach($timeoutWarnings, function ($timeoutWarning) { - new TimeoutWarning($timeoutWarning).init() - }); }) +*/ diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js index b9e0df3a9..cc0f62abd 100644 --- a/app/javascript/controllers/application.js +++ b/app/javascript/controllers/application.js @@ -1,4 +1,4 @@ -import { Application } from "@hotwired/stimulus" +import { Application } from '@hotwired/stimulus' import Reveal from 'stimulus-reveal-controller' const application = Application.start() @@ -7,4 +7,4 @@ application.register('reveal', Reveal) application.debug = true window.Stimulus = application -export { application } \ No newline at end of file +export { application } diff --git a/app/javascript/controllers/autocomplete_controller.js b/app/javascript/controllers/autocomplete_controller.js index 08b389c59..51d9dc075 100644 --- a/app/javascript/controllers/autocomplete_controller.js +++ b/app/javascript/controllers/autocomplete_controller.js @@ -1,4 +1,4 @@ -import { Controller } from "@hotwired/stimulus" +import { Controller } from '@hotwired/stimulus' import accessibleAutocomplete from 'accessible-autocomplete' // Connects to data-controller="autocomplete" diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 06e56f1ea..025a4d132 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -2,7 +2,7 @@ // Run that command whenever you add a new controller or create them with // ./bin/rails generate stimulus controllerName -import { application } from "./application" +import { application } from './application' -import AutocompleteController from "./autocomplete_controller" -application.register("autocomplete", AutocompleteController) +import AutocompleteController from './autocomplete_controller' +application.register('autocomplete', AutocompleteController) diff --git a/app/javascript/timeout-warning.js b/app/javascript/timeout-warning.js deleted file mode 100644 index fdc3c5af9..000000000 --- a/app/javascript/timeout-warning.js +++ /dev/null @@ -1,456 +0,0 @@ -/** - * - * Plugin has been in test by alphagov documentation and code links below - * - * Github repo - * https://github.com/hannalaakso/accessible-timeout-warning - * - * Documentation here - * https://github.com/alphagov/govuk-design-system-backlog/issues/104 - * - * Component which this has been copied and updated from - * https://github.com/hannalaakso/accessible-timeout-warning/tree/master/src/govuk/components/timeout-warning - * - */ - -import axios from 'axios'; - -function TimeoutWarning ($module) { - - this.$module = $module - this.$lastFocusedEl = null - this.$closeButton = $module.querySelector('.js-dialog-close') - this.$cancelButton = $module.querySelector('.js-dialog-cancel') - this.overLayClass = 'govuk-timeout-warning-overlay' - this.$fallBackElement = document.querySelector('.govuk-timeout-warning-fallback') - this.timers = [] - // UI countdown timer specific markup - this.$countdown = $module.querySelector('.timer') - this.$accessibleCountdown = $module.querySelector('.at-timer') - // UI countdown specific settings - this.idleMinutesBeforeTimeOut = $module.getAttribute('data-minutes-idle-timeout') ? $module.getAttribute('data-minutes-idle-timeout') : 25 - this.timeOutRedirectUrl = $module.getAttribute('data-url-redirect') ? $module.getAttribute('data-url-redirect') : 'timeout' - this.minutesTimeOutModalVisible = $module.getAttribute('data-minutes-modal-visible') ? $module.getAttribute('data-minutes-modal-visible') : 5 - this.userLoggedIn = $module.getAttribute('data-user-status') ? $module.getAttribute('data-user-status') : false - this.timeUserLastInteractedWithPage = '' - this.timeUserLastInteractedOnServer = '' - this.timeUserLastInteractedOnServerTimout = '' -} - -// Initialise component -TimeoutWarning.prototype.init = function () { - - // Check for module - if (!this.$module) { - return - } - - // Check that dialog element has native or polyfill support - if (!this.dialogSupported()) { - return - } - - if(this.userLoggedIn == 'false') { - return - } - - // Start watching for idleness - this.countIdleTime() - - this.$closeButton.addEventListener('click', this.closeDialog.bind(this)) - this.$module.addEventListener('keydown', this.escClose.bind(this)) - this.$cancelButton.addEventListener('click', this.redirect.bind(this)) - - // Debugging tip: This event doesn't kick in in Chrome if you have Inspector panel open and have clicked on it - // as it is now the active element. Click on the window to make it active before moving to another tab. - window.addEventListener('focus', this.checkIfShouldHaveTimedOut.bind(this)) -} - -// Check if browser supports native dialog element or can use polyfill -TimeoutWarning.prototype.dialogSupported = function () { - if (typeof HTMLDialogElement === 'function') { - // Native dialog is supported by browser - return true - } else { - // Native dialog is not supported by browser so use polyfill - try { - window.dialogPolyfill.registerDialog(this.$module) - return true - } catch (error) { - // Doesn't support polyfill (IE8) - display fallback element - this.$fallBackElement.classList.add('govuk-!-display-block') - return false - } - } -} - -// Count idle time (user not interacting with page) -// Reset idle time counter when user interacts with the page -// If user is idle for specified time period, open timeout warning as dialog -TimeoutWarning.prototype.countIdleTime = function () { - var idleTime - var milliSecondsBeforeTimeOut = this.idleMinutesBeforeTimeOut * 60000 - - // As user interacts with the page, keep resetting the timer - window.onload = resetIdleTime.bind(this) - window.onmousemove = resetIdleTime.bind(this) - window.onmousedown = resetIdleTime.bind(this) // Catches touchscreen presses - window.onclick = resetIdleTime.bind(this) // Catches touchpad clicks - window.onscroll = resetIdleTime.bind(this) // Catches scrolling with arrow keys - window.onkeypress = resetIdleTime.bind(this) - window.onkeyup = resetIdleTime.bind(this) // Catches Android keypad presses - - function resetIdleTime () { - // As user has interacted with the page, reset idle time - clearTimeout(idleTime) - - // Start new idle time - idleTime = setTimeout(this.openDialog.bind(this), milliSecondsBeforeTimeOut) - - // TO DO - Step A of client/server interaction - // Set last interactive time on server by periodically ping server - // with AJAX when user interacts with client side - // See setLastActiveTimeOnServer() - if (window.localStorage) { - window.localStorage.setItem('timeUserLastInteractedWithPage', new Date()) - } - } -} - -TimeoutWarning.prototype.openDialog = function () { - // TO DO - Step B of client/server interaction - // GET last interactive time from server before showing warning - // User could be interacting with site in 2nd tab - // Update time left accordingly - - if (!this.isDialogOpen()) { - document.querySelector('body').classList.add(this.overLayClass) - this.saveLastFocusedEl() - this.makePageContentInert() - this.$module.showModal() - this.$module.focus() - this.startUiCountdown() - - // if (window.history.pushState) { - // window.history.pushState('', '') // This updates the History API to enable state to be "popped" to detect browser navigation for disableBackButtonWhenOpen - // } - } -} - -// Starts a UI countdown timer. If timer is not cancelled before 0 -// reached + 4 seconds grace period, user is redirected. -TimeoutWarning.prototype.startUiCountdown = function () { - this.clearTimers() // Clear any other modal timers that might have been running - var $module = this - var $countdown = this.$countdown - var $accessibleCountdown = this.$accessibleCountdown - var minutes = this.minutesTimeOutModalVisible - var timerRunOnce = false - var iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream - var timers = this.timers - - var seconds = 60 * minutes - - $countdown.innerHTML = minutes + ' minute' + (minutes > 1 ? 's' : ''); - - (function runTimer () { - var minutesLeft = parseInt(seconds / 60, 10) - var secondsLeft = parseInt(seconds % 60, 10) - var timerExpired = minutesLeft < 1 && secondsLeft < 1 - - var minutesText = minutesLeft > 0 ? '' + minutesLeft + ' minute' + (minutesLeft > 1 ? 's' : '') + '' : ' ' - var secondsText = secondsLeft >= 1 ? ' ' + secondsLeft + ' second' + (secondsLeft > 1 ? 's' : '') + '' : '' - var atMinutesNumberAsText = '' - var atSecondsNumberAsText = '' - - try { - atMinutesNumberAsText = this.numberToWords(minutesLeft) // Attempt to convert numerics into text as iOS VoiceOver ccassionally stalled when encountering numbers - atSecondsNumberAsText = this.numberToWords(secondsLeft) - } catch (e) { - atMinutesNumberAsText = minutesLeft - atSecondsNumberAsText = secondsLeft - } - - var atMinutesText = minutesLeft > 0 ? atMinutesNumberAsText + ' minute' + (minutesLeft > 1 ? 's' : '') + '' : '' - var atSecondsText = secondsLeft >= 1 ? ' ' + atSecondsNumberAsText + ' second' + (secondsLeft > 1 ? 's' : '') + '' : '' - - // Below string will get read out by screen readers every time the timeout refreshes (every 15 secs. See below). - // Please add additional information in the modal body content or in below extraText which will get announced to AT the first time the time out opens - var text = 'For security reasons, you will be signed out of the service in ' + minutesText + secondsText + '.' - var atText = 'For security reasons, you will be signed out of the service in ' + atMinutesText - if (atSecondsText) { - if (minutesLeft > 0) { - atText += ' and' - } - atText += atSecondsText + '.' - } else { - atText += '.' - } - var extraText = '' // leaving this empty as we dont need it - - if (timerExpired) { - // TO DO - client/server interaction - // GET last interactive time from server before timing out user - // to ensure that user hasn’t interacted with site in another tab - - $countdown.innerHTML = 'You are about to be redirected' - $accessibleCountdown.innerHTML = 'You are about to be redirected' - - setTimeout($module.redirect.bind($module), 4000) - } else { - seconds-- - - $countdown.innerHTML = text + extraText - - if (minutesLeft < 1 && secondsLeft < 20) { - $accessibleCountdown.setAttribute('aria-live', 'assertive') - } - - if (!timerRunOnce) { - // Read out the extra content only once. Don't read out on iOS VoiceOver which stalls on the longer text - - if (iOS) { - $accessibleCountdown.innerHTML = atText - } else { - $accessibleCountdown.innerHTML = atText + extraText - } - timerRunOnce = true - } else if (secondsLeft % 15 === 0) { - // Update screen reader friendly content every 15 secs - $accessibleCountdown.innerHTML = atText - } - - // TO DO - client/server interaction - // GET last interactive time from server while the warning is being displayed. - // If user interacts with site in second tab, warning should be dismissed. - // Compare what server returned to what is stored in client - // If needed, call this.closeDialog() - - // JS doesn't allow resetting timers globally so timers need to be retained for resetting. - timers.push(setTimeout(runTimer, 1000)) - } - })() -} - -TimeoutWarning.prototype.saveLastFocusedEl = function () { - this.$lastFocusedEl = document.activeElement - if (!this.$lastFocusedEl || this.$lastFocusedEl === document.body) { - this.$lastFocusedEl = null - } else if (document.querySelector) { - this.$lastFocusedEl = document.querySelector(':focus') - } -} - -// Set focus back on last focused el when modal closed -TimeoutWarning.prototype.setFocusOnLastFocusedEl = function () { - if (this.$lastFocusedEl) { - window.setTimeout(function () { - this.$lastFocusedEl.focus() - }, 0) - } -} - -// Set page content to inert to indicate to screenreaders it's inactive -// NB: This will look for #content for toggling inert state -TimeoutWarning.prototype.makePageContentInert = function () { - if (document.querySelector('#content')) { - document.querySelector('#content').inert = true - document.querySelector('#content').setAttribute('aria-hidden', 'true') - } -} - -// Make page content active when modal is not open -// NB: This will look for #content for toggling inert state -TimeoutWarning.prototype.removeInertFromPageContent = function () { - if (document.querySelector('#content')) { - document.querySelector('#content').inert = false - document.querySelector('#content').setAttribute('aria-hidden', 'false') - } -} - -TimeoutWarning.prototype.isDialogOpen = function () { - return this.$module['open'] -} - -TimeoutWarning.prototype.closeDialog = function () { - - if (this.isDialogOpen()) { - document.querySelector('body').classList.remove(this.overLayClass) - this.$module.close() - this.setFocusOnLastFocusedEl() - this.removeInertFromPageContent() - - this.clearTimers() - this.resetSessionTimeout() - this.pollForSessionTimeout() - } -} - -// Clears modal timer -TimeoutWarning.prototype.clearTimers = function () { - for (var i = 0; i < this.timers.length; i++) { - clearTimeout(this.timers[i]) - } -} - -TimeoutWarning.prototype.disableBackButtonWhenOpen = function () { - window.addEventListener('popstate', function () { - if (this.isDialogOpen()) { - this.closeDialog() - } else { - window.history.go(-1) - } - }) -} - -// Close modal when ESC pressed -TimeoutWarning.prototype.escClose = function (event) { - // get the target element - if (this.isDialogOpen() && event.keyCode === 27) { - this.closeDialog() - } -} - -// Do a timestamp comparison with server when the page regains focus to check -// if the user should have been timed out already. -// This could happen but because the computer went to sleep, the browser crashed etc. -TimeoutWarning.prototype.checkIfShouldHaveTimedOut = function () { - if (window.localStorage) { - // TO DO - client/server interaction - // GET last interactive time from server before timing out user - // to ensure that user hasn’t interacted with site in another tab - - // If less time than data-minutes-idle-timeout left, call this.openDialog.bind(this) - var timeUserLastInteractedWithPage = new Date(window.localStorage.getItem('timeUserLastInteractedWithPage')) - - var seconds = Math.abs((timeUserLastInteractedWithPage - new Date()) / 1000) - - // TO DO: use both idlemin and timemodalvisible - if (seconds > this.idleMinutesBeforeTimeOut * 60) { - // if (seconds > 60) { - this.redirect.bind(this) - } - } -} -TimeoutWarning.prototype.redirect = function () { - window.location.replace(this.timeOutRedirectUrl) -} -// Example function for sending last active time of user to server -TimeoutWarning.prototype.setLastActiveTimeOnServer = function () { - // var xhttp = new XMLHttpRequest() - // xhttp.onreadystatechange = function () { - // if (this.readyState === 4 && this.status === 200) { - // var timeUserLastInteractedWithPage = new Date() - // } - // } - // - // xhttp.open('POST', 'update-time-user-interacted-with-page.rb', true) - // xhttp.send() -} - -TimeoutWarning.prototype.numberToWords = function () { - var string = n.toString() - var units - var tens - var scales - var start - var end - var chunks - var chunksLen - var chunk - var ints - var i - var word - var words = 'and' - - if (parseInt(string) === 0) { - return 'zero' - } - - /* Array of units as words */ - units = [ '', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen' ] - - /* Array of tens as words */ - tens = [ '', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety' ] - - /* Array of scales as words */ - scales = [ '', 'thousand', 'million', 'billion', 'trillion', 'quadrillion', 'quintillion', 'sextillion', 'septillion', 'octillion', 'nonillion', 'decillion', 'undecillion', 'duodecillion', 'tredecillion', 'quatttuor-decillion', 'quindecillion', 'sexdecillion', 'septen-decillion', 'octodecillion', 'novemdecillion', 'vigintillion', 'centillion' ] - - /* Split user arguemnt into 3 digit chunks from right to left */ - start = string.length - chunks = [] - while (start > 0) { - end = start - chunks.push(string.slice((start = Math.max(0, start - 3)), end)) - } - - /* Check if function has enough scale words to be able to stringify the user argument */ - chunksLen = chunks.length - if (chunksLen > scales.length) { - return '' - } - - /* Stringify each integer in each chunk */ - words = [] - for (i = 0; i < chunksLen; i++) { - chunk = parseInt(chunks[i]) - - if (chunk) { - /* Split chunk into array of individual integers */ - ints = chunks[i].split('').reverse().map(parseFloat) - - /* If tens integer is 1, i.e. 10, then add 10 to units integer */ - if (ints[1] === 1) { - ints[0] += 10 - } - - /* Add scale word if chunk is not zero and array item exists */ - if ((word = scales[i])) { - words.push(word) - } - - /* Add unit word if array item exists */ - if ((word = units[ints[0]])) { - words.push(word) - } - - /* Add tens word if array item exists */ - if ((word = tens[ ints[1] ])) { - words.push(word) - } - - /* Add hundreds word if array item exists */ - if ((word = units[ints[2]])) { - words.push(word + ' hundred') - } - } - } - return words.reverse().join(' ') -} - -TimeoutWarning.prototype.pollForSessionTimeout = function() { - axios.get('/check_session_timeout') - .then((response) => { - if(response.data){ - window.localStorage.setItem('timeUserLastInteractedWithPage', new Date()) - } - }) - .catch(error => console.log(error)); -} - -TimeoutWarning.prototype.resetSessionTimeout = function() { - axios.get('/extend_session') - .then((response) => {}) - .catch(error => console.log(error)); -} - -TimeoutWarning.prototype.serverResponse = function(responseServer) { - this.timeUserLastInteractedOnServer = responseServer; -} - -TimeoutWarning.prototype.serverResponsePoll = function(responseServer) { - clearTimeout(this.timeUserLastInteractedOnServerTimout) - this.timeUserLastInteractedOnServerTimout = setTimeout(this.pollForSessionTimeout(), 200); -} - -export default TimeoutWarning; diff --git a/app/views/layouts/_flash.html.slim b/app/views/layouts/_flash.html.slim index ad06ac978..cf0f2c496 100644 --- a/app/views/layouts/_flash.html.slim +++ b/app/views/layouts/_flash.html.slim @@ -2,4 +2,5 @@ .govuk-grid-row .govuk-grid-column-full - flash.each do |variant, message| - = govuk_notification_banner title_text: t("banners.#{variant}"), text: m(message), success: %w[notice].include?(variant), classes: "govuk-notification-banner--#{variant}" + - unless %w{timedout}.include?(variant) + = govuk_notification_banner title_text: t("banners.#{variant}"), text: m(message), success: %w[notice].include?(variant), classes: "govuk-notification-banner--#{variant}" diff --git a/app/views/layouts/_timeout.html.slim b/app/views/layouts/_timeout.html.slim deleted file mode 100644 index 929cb9eb1..000000000 --- a/app/views/layouts/_timeout.html.slim +++ /dev/null @@ -1,36 +0,0 @@ -- if debug? - pre.debug_dump - p Warning: opening a browser inspector may impact this popup - ul - li timeout enabled: #{timeout_active} - li timeout after: #{timeout_duration} minutes - li timeout warning: #{timeout_warning} minutes - li timeout modal visible for: #{timeout_modal} minutes - li timeout at: #{timeout_time} - / li timer start value: #{timeout_timer} in seconds - - ul - li= link_to 'Timed out page (WARNING: this ends the session)', users_timeout_path - li= link_to 'Countdown timer', check_session_timeout_path - li= link_to 'Extend timer', extend_session_path - -/[if lt IE 10] - script[src="https://cdnjs.cloudflare.com/ajax/libs/dialog-polyfill/0.5.0/dialog-polyfill.js"] - -- if user_signed_in? - .govuk-timeout-warning-fallback - .govuk-inset-text= m('timeout.noscript', timeout_duration: timeout_duration, sign_out_time: timeout_time) - -dialog#js-timeout-warning.govuk-timeout-warning.dialog data-module='govuk-timeout-warning' tabindex=0 data-user-status=timeout_active data-minutes-idle-timeout=timeout_warning data-minutes-modal-visible=timeout_modal data-url-redirect=users_timeout_path role='dialog' aria-live='polite' aria-labelledby='dialog-title' aria-describedby='at-timer' - - h1#dialog-title.govuk-heading-l= t('timeout.heading') - - .govuk-body.timer class='govuk-!-static-margin-bottom-5' aria-hidden='true' aria-relevant='additions' - #at-timer.at-timer.govuk-visually-hidden role='status' class='govuk-!-static-margin-bottom-5' - - p.govuk-body= t('timeout.body') - - div class='govuk-!-static-margin-bottom-5' - button.govuk-button.dialog-button.js-dialog-close data-module='govuk-button' Stay signed in - div - = govuk_link_to 'Sign out now', '#', class: 'govuk-!-margin-top-5 js-dialog-cancel' diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index e8b7f9c8d..b87d0ff55 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -17,8 +17,6 @@ html.govuk-template lang='en' .govuk-width-container = render 'layouts/banner' - .noprint = render 'layouts/timeout' - main#main-content.govuk-main-wrapper role='main' = render 'layouts/flash' diff --git a/app/views/layouts/hero.html.slim b/app/views/layouts/hero.html.slim index 29b41d91f..052f9ce53 100644 --- a/app/views/layouts/hero.html.slim +++ b/app/views/layouts/hero.html.slim @@ -18,7 +18,6 @@ html.govuk-template lang='en' .dfe-width-container = render 'layouts/banner' - .noprint= render 'layouts/timeout' .noprint= back_link = render 'layouts/flash' diff --git a/app/views/timeout/timeout_user.html.slim b/app/views/timeout/timeout_user.html.slim deleted file mode 100644 index adcd94bdf..000000000 --- a/app/views/timeout/timeout_user.html.slim +++ /dev/null @@ -1,7 +0,0 @@ -- content_for :page_title do - = html_title t('timeout.title') - -h1.govuk-heading-l For your security, we signed you out - -p= "You have not done anything for #{timeout_warning} minutes." -p= link_to 'Sign in', new_user_session_path, class: 'govuk-button' diff --git a/config/application.rb b/config/application.rb index 8f5434d0c..9825b1f60 100644 --- a/config/application.rb +++ b/config/application.rb @@ -51,12 +51,9 @@ class Application < Rails::Application config.google_analytics_tracking_id = ENV.fetch('TRACKING_ID', '#TRACKING_ID_env_var_missing') config.hotjar_site_id = ENV.fetch('HOTJAR_SITE_ID', '#HOTJAR_SITE_ID_env_var_missing') - # Timeout - # user_timeout_warning_minutes and user_timeout_modal_visible value combined must be lower than user_timeout_minutes - config.unlock_in_minutes = ENV.fetch('UNLOCK_IN_MINUTES', '120').to_i - config.user_timeout_minutes = ENV.fetch('TIMEOUT_MINUTES', '60').to_i - config.user_timeout_warning_minutes = ENV.fetch('TIMEOUT_WARNING_MINUTES', '55').to_i - config.user_timeout_modal_visible = ENV.fetch('TIMEOUT_MODAL_VISIBLE', '5').to_i + # Devise + config.unlock_in_minutes = ENV.fetch('UNLOCK_IN_MINUTES', '120').to_i + config.timeout_in_minutes = ENV.fetch('TIMEOUT_IN_MINUTES', '60').to_i # Contentful config.contentful_space = ENV.fetch('CONTENTFUL_SPACE', credentials.dig(:contentful, :space)) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 47183497e..926c415b5 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -9,45 +9,6 @@ # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. -class CustomFailureApp < Devise::FailureApp - # @see TimeoutController#timeout_user - def redirect - store_location! - message = warden.message || warden_options[:message] - if message == :timeout - # NB: redirection closes session without JS and negates a flash message - redirect_to users_timeout_path - else - super - end - end - -protected - - # Patched to pass {unlock_in} to 'en.devise.failure.locked' - # - # @return [String] - def i18n_message(default = nil) - message = warden_message || default || :unauthenticated - - if message.is_a?(Symbol) - options = {} - options[:resource_name] = scope - options[:scope] = 'devise.failure' - options[:unlock_in] = Devise.unlock_in.in_hours.to_i - options[:default] = [message] - auth_keys = scope_class.authentication_keys - keys = (auth_keys.respond_to?(:keys) ? auth_keys.keys : auth_keys).map { |key| scope_class.human_attribute_name(key) } - options[:authentication_keys] = keys.join(I18n.translate(:"support.array.words_connector")) - options = i18n_options(options) - - I18n.t(:"#{scope}.#{message}", **options) - else - message.to_s - end - end -end - Devise.setup do |config| # The secret key used by Devise. Devise uses this key to generate # random tokens. Changing this key will render invalid all existing @@ -225,7 +186,7 @@ def i18n_message(default = nil) # ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. After this # time the user will be asked for credentials again. Default is 30 minutes. - config.timeout_in = Rails.configuration.user_timeout_minutes.minutes + config.timeout_in = Rails.configuration.timeout_in_minutes.minutes # ==> Configuration for :lockable # Defines which strategy will be used to lock an account. @@ -326,11 +287,10 @@ def i18n_message(default = nil) # If you want to use other strategies, that are not supported by Devise, or # change the failure app, you can configure them inside the config.warden block. # - config.warden do |manager| - manager.failure_app = CustomFailureApp - # manager.intercept_401 = false - # manager.default_strategies(scope: :user).unshift :some_external_strategy - end + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + # end # ==> Mountable engine configurations # When using Devise inside an engine, let's call it `MyEngine`, and this engine diff --git a/config/locales/en.yml b/config/locales/en.yml index 33e387968..0fd2b734b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -273,17 +273,6 @@ en: my_learning_log: title: Learning log - # /user/timeout - # and modal - timeout: - title: User timeout - heading: You're about to be signed out - body: Your progress through the training has been saved. Other changes made to your account or learning log will not be saved. - noscript: | - For security, you'll be signed out at %{sign_out_time}. - - To stay signed in, continue through the training or refresh the page. - # Account management --------------------------------------------------------- # /my-account diff --git a/config/routes.rb b/config/routes.rb index 6dd20b2c8..ce1610819 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,7 @@ Rails.application.routes.draw do root 'home#index' - get 'health', to: 'home#show' - get 'audit', to: 'home#audit' + get 'health', to: 'home#show' + get 'audit', to: 'home#audit' get 'my-modules', to: 'learning#show' # @see User#course get '404', to: 'errors#not_found', via: :all @@ -20,27 +20,22 @@ sign_out: 'sign-out', } - # @see TimeoutWarning js component - # @note these path names are required devise_scope :user do - get 'check_session_timeout', to: 'timeout#check' - get 'extend_session', to: 'timeout#extend' - get 'users/timeout', to: 'timeout#timeout_user' get 'users/sign_out', to: 'users/sessions#destroy' - get 'users/review', to: 'users/sessions#sign_in_test_user' unless Rails.application.live? + get 'users/review', to: 'users/sessions#sign_in_test_user' unless Rails.application.live? end namespace :registration do - resource :terms_and_conditions, only: %i[edit update], path: 'terms-and-conditions' - resource :name, only: %i[edit update] - resource :setting_type, only: %i[edit update], path: 'setting-type' - resource :setting_type_other, only: %i[edit update], path: 'setting-type-other' - resource :local_authority, only: %i[edit update], path: 'local-authority' - resource :role_type, only: %i[edit update], path: 'role-type' - resource :role_type_other, only: %i[edit update], path: 'role-type-other' + resource :terms_and_conditions, only: %i[edit update], path: 'terms-and-conditions' + resource :name, only: %i[edit update] + resource :setting_type, only: %i[edit update], path: 'setting-type' + resource :setting_type_other, only: %i[edit update], path: 'setting-type-other' + resource :local_authority, only: %i[edit update], path: 'local-authority' + resource :role_type, only: %i[edit update], path: 'role-type' + resource :role_type_other, only: %i[edit update], path: 'role-type-other' resource :early_years_experience, only: %i[edit update], path: 'early-years-experience' - resource :training_emails, only: %i[edit update], path: 'training-emails' - resource :early_years_emails, only: %i[edit update], path: 'early-years-emails' + resource :training_emails, only: %i[edit update], path: 'training-emails' + resource :early_years_emails, only: %i[edit update], path: 'early-years-emails' end resource :user, controller: :user, only: %i[show], path: 'my-account' do @@ -70,18 +65,18 @@ get 'structure', to: 'debug#show' end - resources :pages, only: %i[index show], path: 'content-pages' - resources :questions, only: %i[show], path: 'questionnaires' - resources :responses, only: %i[update] - resources :assessments, only: %i[show new], path: 'assessment-result' + resources :pages, only: %i[index show], path: 'content-pages' + resources :questions, only: %i[show], path: 'questionnaires' + resources :assessments, only: %i[show new], path: 'assessment-result' + resources :responses, only: %i[update] end end post 'change', to: 'hook#change' post 'release', to: 'hook#release' - post 'notify', to: 'hook#notify' # if Rails.application.live? + post 'notify', to: 'hook#notify' - get 'about-training', to: 'about#course', as: :course_overview + get 'about-training', to: 'about#course', as: :course_overview get 'about/the-experts', to: 'about#experts', as: :experts resources :about, only: %i[show], path: 'about' diff --git a/config/sitemap.rb b/config/sitemap.rb index 738b2d8e9..8c0338d3b 100644 --- a/config/sitemap.rb +++ b/config/sitemap.rb @@ -39,8 +39,6 @@ # add '/422' add '/500' - add users_timeout_path - add course_overview_path add experts_path diff --git a/docker-compose.qa.yml b/docker-compose.qa.yml index 0dd4c5d38..31b15b465 100644 --- a/docker-compose.qa.yml +++ b/docker-compose.qa.yml @@ -16,7 +16,6 @@ services: # overrides - WORKSPACE - USER_PASSWORD - - TIMEOUT_MINUTES - CONTENTFUL_ENVIRONMENT - CONTENTFUL_PREVIEW volumes: diff --git a/spec/config_spec.rb b/spec/config_spec.rb index 418db469f..d5db78dbc 100644 --- a/spec/config_spec.rb +++ b/spec/config_spec.rb @@ -41,14 +41,6 @@ end end - describe 'time out' do - it 'sets interval in minutes' do - expect(config.user_timeout_minutes).to eq 60 - expect(config.user_timeout_warning_minutes).to eq 55 - expect(config.user_timeout_modal_visible).to eq 5 - end - end - describe 'pages accessible even when in maintenance mode' do specify do expect(config.protected_endpoints).to eq %w[ diff --git a/spec/controllers/training/modules_controller_spec.rb b/spec/controllers/training/modules_controller_spec.rb deleted file mode 100644 index 451975dd4..000000000 --- a/spec/controllers/training/modules_controller_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'rails_helper' - -RSpec.describe Training::ModulesController, type: :controller do - # Using a controller spec as need to access session to test timeout - describe 'user timeout' do - before do - sign_in create(:user, :registered) - end - - it 'allows user to access target page' do - get :show, params: { id: 'alpha' } - expect(response).to have_http_status(:success) - end - - context 'when session has timed out' do - let(:timeout) do - Rails.configuration.user_timeout_minutes.minutes + 1.second - end - - it 'redirects to timeout error' do - get :show, params: { id: 'alpha' }, session: { - 'warden.user.user.session' => { - 'last_request_at' => timeout.ago, - }, - } - expect(response).to redirect_to users_timeout_path - end - end - end -end diff --git a/spec/lib/seed_snippets_spec.rb b/spec/lib/seed_snippets_spec.rb index 6f7470ea2..49ec5bfd8 100644 --- a/spec/lib/seed_snippets_spec.rb +++ b/spec/lib/seed_snippets_spec.rb @@ -5,7 +5,7 @@ subject(:locales) { described_class.new.call } it 'converts all translations' do - expect(locales.count).to be 191 + expect(locales.count).to be 187 end it 'dot separated key -> Page::Resource#name' do diff --git a/spec/requests/static_spec.rb b/spec/requests/static_spec.rb index b46764877..4b846085d 100644 --- a/spec/requests/static_spec.rb +++ b/spec/requests/static_spec.rb @@ -16,8 +16,6 @@ specify { expect('/terms-and-conditions').to be_successful } - specify { expect('/users/timeout').to be_successful } - specify { expect('/wifi-and-data').to be_successful } end diff --git a/spec/requests/timeout_warning_spec.rb b/spec/requests/timeout_warning_spec.rb deleted file mode 100644 index fd6247634..000000000 --- a/spec/requests/timeout_warning_spec.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'Session timeout warning', type: :request do - describe '#check' do - context 'with an active session' do - before do - sign_in create(:user, :registered) - get check_session_timeout_path - end - - # lag may have lowered the counter when the assertion is made - it 'reports seconds before timeout' do - expect(response).to have_http_status(:success) - expect(response.body.to_i).to be_within(1).of(3600) # ~60*60 - end - - it 'decreases over time' do - travel_to 1.minute.from_now - get check_session_timeout_path - expect(response.body.to_i).to be_within(1).of(3540) # ~59*60 - end - end - - context 'without an active session' do - before do - get check_session_timeout_path - end - - it 'returns zero' do - expect(response).to have_http_status(:success) - expect(response.body).to eq '0' - end - end - end - - describe '#extend' do - context 'with an active session' do - before do - sign_in create(:user, :registered) - get extend_session_path - end - - it 'extends current session' do - expect(response).to have_http_status(:success) - expect(response.body.to_i).to be_within(1).of(3600) # ~59*60 - end - end - - context 'without an active session' do - before do - get extend_session_path - end - - it 'returns zero' do - expect(response).to have_http_status(:success) - expect(response.body).to eq '0' - end - end - end -end diff --git a/spec/system/page_title_spec.rb b/spec/system/page_title_spec.rb index c112a10ed..c9a65e0f3 100644 --- a/spec/system/page_title_spec.rb +++ b/spec/system/page_title_spec.rb @@ -33,7 +33,6 @@ it { expect(user_path).to have_page_title('My account') } it { expect(my_modules_path).to have_page_title('My modules') } it { expect(course_overview_path).to have_page_title('About training') } - it { expect(users_timeout_path).to have_page_title('User timeout') } it { expect(setting_path('cookie-policy')).to have_page_title('Cookie policy') } it { expect(edit_registration_terms_and_conditions_path).to have_page_title('Terms and Conditions') } diff --git a/spec/system/timeout_modal_spec.rb b/spec/system/timeout_modal_spec.rb deleted file mode 100644 index dbddd9243..000000000 --- a/spec/system/timeout_modal_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'rails_helper' - -RSpec.describe 'Timeout modal' do - context 'with an authenticated user' do - include_context 'with user' - - context 'and javascript enabled ' do - it 'data attribute user status' do - expect(page).to have_selector("#js-timeout-warning[@data-user-status='true']") - end - - it 'data attribute minutes modal to popup to user after no activity' do - expect(page).to have_selector('#js-timeout-warning[@data-minutes-idle-timeout]') - end - - it 'data attribute minutes modal to be displayed to user' do - expect(page).to have_selector('#js-timeout-warning[@data-minutes-modal-visible]') - end - - it 'data attributes timeout modal combined should be less than user_timeout_minutes' do - data_minutes_idle_timeout = page.find('#js-timeout-warning')['data-minutes-idle-timeout'] - data_minutes_modal_visible = page.find('#js-timeout-warning')['data-minutes-modal-visible'] - expect(Rails.configuration.user_timeout_minutes).to be >= (data_minutes_idle_timeout.to_i + data_minutes_modal_visible.to_i) - end - end - - context 'and javascript disabled', js: false do - it 'displays a timeout message' do - expect(page).to have_content 'For security, you’ll be signed out at' - end - end - end - - context 'with an unauthenticated user' do - before do - visit '/' - end - - context 'and javascript enabled' do - it 'data attribute user status' do - expect(page).to have_selector("#js-timeout-warning[@data-user-status='false']") - end - end - - context 'and javascript disabled' do - it 'does not display a timeout message', js: false do - expect(page).not_to have_content 'For security, you’ll be signed out at' - end - end - end -end