renalware-core 2.0.148 → 2.0.149

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/renalware/components/session_timeout_redirect.js.erb +26 -21
  3. data/app/assets/javascripts/renalware/core.js.erb +2 -0
  4. data/app/assets/javascripts/renalware/rollup_compiled.js +169 -0
  5. data/app/assets/stylesheets/renalware/partials/_simple_form.scss +2 -2
  6. data/app/components/renalware/hd/administer_prescription_dropdown_component.html.slim +1 -1
  7. data/app/controllers/renalware/base_controller.rb +3 -3
  8. data/app/controllers/renalware/session_timeout_controller.rb +20 -17
  9. data/app/javascript/renalware/controllers/session_controller.js +223 -0
  10. data/app/javascript/renalware/index.js +2 -0
  11. data/app/models/renalware/accesses/assessment.rb +5 -1
  12. data/app/models/renalware/accesses/procedure.rb +4 -1
  13. data/app/models/renalware/accesses/profile.rb +4 -1
  14. data/app/models/renalware/clinical/body_composition.rb +4 -1
  15. data/app/models/renalware/clinical/dry_weight.rb +4 -1
  16. data/app/models/renalware/clinics/clinic_visit.rb +5 -1
  17. data/app/models/renalware/hd/preference_set.rb +4 -1
  18. data/app/models/renalware/hd/profile.rb +5 -2
  19. data/app/models/renalware/hd/session.rb +4 -1
  20. data/app/models/renalware/low_clearance/profile.rb +5 -2
  21. data/app/models/renalware/medications/prescription.rb +4 -2
  22. data/app/models/renalware/pathology/code_group.rb +3 -1
  23. data/app/models/renalware/pathology/code_group_membership.rb +4 -1
  24. data/app/models/renalware/patient.rb +4 -1
  25. data/app/models/renalware/patients/worry.rb +5 -1
  26. data/app/models/renalware/problems/problem.rb +5 -1
  27. data/app/models/renalware/renal/aki_alert.rb +4 -1
  28. data/app/models/renalware/renal/profile.rb +4 -1
  29. data/app/models/renalware/transplants/donation.rb +4 -2
  30. data/app/models/renalware/transplants/donor_followup.rb +4 -2
  31. data/app/models/renalware/transplants/donor_operation.rb +4 -2
  32. data/app/models/renalware/transplants/donor_workup.rb +4 -2
  33. data/app/models/renalware/transplants/recipient_followup.rb +5 -2
  34. data/app/models/renalware/transplants/recipient_operation.rb +5 -2
  35. data/app/models/renalware/transplants/recipient_workup.rb +5 -2
  36. data/app/models/renalware/transplants/registration.rb +5 -2
  37. data/app/models/renalware/transplants/rejection_episode.rb +1 -1
  38. data/app/models/renalware/ukrdc/{batch_number.rb → batch.rb} +1 -1
  39. data/app/models/renalware/ukrdc/create_encrypted_patient_xml_files.rb +7 -10
  40. data/app/models/renalware/ukrdc/create_patient_xml_file.rb +3 -4
  41. data/app/models/renalware/ukrdc/housekeeping/remove_old_export_archive_folders.rb +3 -3
  42. data/app/models/renalware/ukrdc/housekeeping/remove_stale_files.rb +2 -2
  43. data/app/models/renalware/ukrdc/incoming/import_surveys.rb +1 -2
  44. data/app/models/renalware/ukrdc/transmission_log.rb +3 -2
  45. data/app/presenters/renalware/user_session_presenter.rb +44 -0
  46. data/app/views/renalware/admissions/consults/_form.html.slim +19 -18
  47. data/app/views/renalware/hd/prescription_administrations/_form.html.slim +1 -1
  48. data/app/views/renalware/hd/prescription_administrations/_row.html.slim +1 -1
  49. data/app/views/renalware/hd/prescription_administrations/new.js.erb +1 -1
  50. data/app/views/renalware/hd/protocols/_protocol.pdf.slim +39 -38
  51. data/app/views/renalware/hd/scheduling/diary_slots/_slot.html.slim +2 -1
  52. data/app/views/renalware/hd/witnesses/_form.html.slim +2 -2
  53. data/app/views/renalware/letters/contacts/_contact.html.slim +1 -1
  54. data/app/views/renalware/letters/letters/index.html.slim +4 -0
  55. data/app/views/renalware/medications/prescriptions/index.html.slim +1 -1
  56. data/app/views/renalware/navigation/_sign_out.html.slim +5 -1
  57. data/app/views/renalware/patients/patients/show/_primary_care_physician.html.slim +1 -1
  58. data/app/views/renalware/transplants/wait_lists/show.html.slim +1 -1
  59. data/config/initializers/paper_trail.rb +1 -1
  60. data/config/initializers/simple_form_wrappers.rb +14 -9
  61. data/config/routes/system.rb +2 -1
  62. data/db/migrate/20200408131217_associate_batch_with_ukrdc_transmission_log.rb +15 -0
  63. data/db/seeds/seeds_helper.rb +8 -2
  64. data/lib/renalware/configuration.rb +6 -0
  65. data/lib/renalware/engine.rb +2 -1
  66. data/lib/renalware/version.rb +1 -1
  67. data/spec/support/shared_examples/supersedable_examples.rb +2 -2
  68. metadata +22 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc532fe594f3e9543c134b831af132696f44881c88673d0aacbaaa78a3f9022e
4
- data.tar.gz: d15adf0bcb79edb855647ba4e73e1a7785cd0771b438348d6a1ff66032158b5c
3
+ metadata.gz: f6f402017b657476cbd3fbb3586210987688063fe3168b529fe9dca935b9ce11
4
+ data.tar.gz: 45f068499baf243025d6e1a09b9c0a685dc86d59dc07e3c2c6c0517b5c12adf5
5
5
  SHA512:
6
- metadata.gz: 7c4cdc7709741aa5c19405fea7e84b96b6c11b11f0f569dc254ec11c9e1cd2a0e48d99a6dfb736040f8a60d0c1f494dbd40cddf1d823c0aa858ecd9ff37b50b1
7
- data.tar.gz: aac97b39b0b13eb5a2d52bf80f3e22666fc325affc4f6fd5ea1b3cce0fdd43cb75604fb5521fe1162f2d867a8afe448ed92d4c8474dca13b00fc6a767ec82bd6
6
+ metadata.gz: 3396ecd803818a8fc285dc69f619cbd455ebda2f0788c54790e43488e8dd6c09f33ed954cc1188404b0f40c0f4127b5c3b26c96a15a14e0b7bcae6e4b57476ce
7
+ data.tar.gz: a241e179da4a434923a13f6eae4c5267880c0d375311322d580cb06e892877cade40d00af4263773e0a6b8deaf922db16c2cd366c4ba6154af118e6e8a30deab
@@ -6,27 +6,32 @@
6
6
  // Note we don't want to poll if we are sat on the login page anyway. For one thing on Heroku it
7
7
  // would prevent a dyno sleeping, but its also a waste of resources.
8
8
  //
9
- $(document).ready(function() {
10
- var login_path ="<%= Renalware::Engine.routes.url_helpers.new_user_session_path %>";
11
- var defaultPollFreq = <%= Renalware.config.session_timeout_polling_frequency.to_i %>;
12
- var frequency_s = defaultPollFreq;
9
+ // Note this mechanism has been superceded by the session_timeout.js stimulus controller.
10
+ //
11
+
12
+ <% if Renalware.config.session_expiry_use_previous_mechansim %>
13
+ $(document).ready(function() {
14
+ var login_path ="<%= Renalware::Engine.routes.url_helpers.new_user_session_path %>";
15
+ var defaultPollFreq = <%= Renalware.config.session_timeout_polling_frequency.to_i %>;
16
+ var frequency_s = defaultPollFreq;
13
17
 
14
- // This is a global window function so that we can call it directly from capybara tests to
15
- // bypass having to wait for the session polling interval to tick over.
16
- window.sessionTimeoutCheck = function(){
17
- if(window.location.pathname != login_path) {
18
- Rails.ajax({
19
- type: "GET",
20
- url: "<%= Renalware::Engine.routes.url_helpers.session_timed_out_path %>",
21
- dataType: "html",
22
- error: function(responseText, status, xhr) {
23
- if (xhr.status == 401) {
24
- window.location.reload()
18
+ // This is a global window function so that we can call it directly from capybara tests to
19
+ // bypass having to wait for the session polling interval to tick over.
20
+ window.sessionTimeoutCheck = function(){
21
+ if(window.location.pathname != login_path) {
22
+ Rails.ajax({
23
+ type: "GET",
24
+ url: "<%= Renalware::Engine.routes.url_helpers.check_session_expired_path %>",
25
+ dataType: "html",
26
+ error: function(responseText, status, xhr) {
27
+ if (xhr.status == 401) {
28
+ window.location.reload()
29
+ }
25
30
  }
26
- }
27
- });
28
- }
29
- };
31
+ });
32
+ }
33
+ };
30
34
 
31
- setInterval(window.sessionTimeoutCheck, (frequency_s * 1000));
32
- });
35
+ setInterval(window.sessionTimeoutCheck, (frequency_s * 1000));
36
+ });
37
+ <% end %>
@@ -57,3 +57,5 @@ window.console = window.console || { log: function() {} };
57
57
  <% if Rails.env.test? %>
58
58
  $.fx.off = true;
59
59
  <% end %>
60
+
61
+ $.fn.select2.defaults.set( "width", "100%" );
@@ -4036,6 +4036,173 @@ var _default$6 = function(_Controller) {
4036
4036
 
4037
4037
  _defineProperty(_default$6, "targets", [ "chart" ]);
4038
4038
 
4039
+ var Rails$1 = window.Rails;
4040
+
4041
+ var _ = window._;
4042
+
4043
+ var _default$7 = function(_Controller) {
4044
+ _inherits(_default, _Controller);
4045
+ var _super = _createSuper(_default);
4046
+ function _default() {
4047
+ var _this;
4048
+ _classCallCheck(this, _default);
4049
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
4050
+ args[_key] = arguments[_key];
4051
+ }
4052
+ _this = _super.call.apply(_super, [ this ].concat(args));
4053
+ _defineProperty(_assertThisInitialized(_this), "checkForSessionExpiryTimeout", null);
4054
+ _defineProperty(_assertThisInitialized(_this), "userActivityDetected", false);
4055
+ _defineProperty(_assertThisInitialized(_this), "checkAlivePath", null);
4056
+ _defineProperty(_assertThisInitialized(_this), "keepAlivePath", null);
4057
+ _defineProperty(_assertThisInitialized(_this), "loginPath", null);
4058
+ _defineProperty(_assertThisInitialized(_this), "throttledRegisterUserActivity", null);
4059
+ _defineProperty(_assertThisInitialized(_this), "sessionTimeoutSeconds", 0);
4060
+ _defineProperty(_assertThisInitialized(_this), "defaultSessionTimeoutSeconds", 20 * 60);
4061
+ _defineProperty(_assertThisInitialized(_this), "throttlePeriodSeconds", 0);
4062
+ _defineProperty(_assertThisInitialized(_this), "defaultThrottlePeriodSeconds", 20);
4063
+ return _this;
4064
+ }
4065
+ _createClass(_default, [ {
4066
+ key: "initialize",
4067
+ value: function initialize() {
4068
+ this.throttlePeriodSeconds = parseInt(this.data.get("register-user-activity-after") || this.defaultThrottlePeriodSeconds);
4069
+ this.sessionTimeoutSeconds = parseInt(this.data.get("timeout") || this.defaultSessionTimeoutSeconds);
4070
+ this.sessionTimeoutSeconds += 10;
4071
+ this.checkAlivePath = this.data.get("check-alive-path");
4072
+ this.loginPath = this.data.get("login-path");
4073
+ this.keepAlivePath = this.data.get("keep-alive-path");
4074
+ this.logSettings();
4075
+ this.throttledRegisterUserActivity = _.throttle(this.registerUserActivity.bind(this), this.throttlePeriodSeconds * 1e3, {
4076
+ leading: false,
4077
+ trailing: true
4078
+ });
4079
+ }
4080
+ }, {
4081
+ key: "connect",
4082
+ value: function connect() {
4083
+ if (this.onLoginPage) {
4084
+ this.log("connect: onLoginPage - skipping session time");
4085
+ } else {
4086
+ this.addHandlersToMonitorUserActivity();
4087
+ this.resetCheckForSessionExpiryTimeout(this.sessionTimeoutSeconds);
4088
+ }
4089
+ }
4090
+ }, {
4091
+ key: "disconnect",
4092
+ value: function disconnect() {
4093
+ if (!this.onLoginPage) {
4094
+ this.removeUserActivityHandlers();
4095
+ clearTimeout(this.checkForSessionExpiryTimeout);
4096
+ }
4097
+ }
4098
+ }, {
4099
+ key: "sendLogoutMessageToAnyOpenTabs",
4100
+ value: function sendLogoutMessageToAnyOpenTabs() {
4101
+ window.localStorage.setItem("logout-event", "logout" + Math.random());
4102
+ }
4103
+ }, {
4104
+ key: "registerUserActivity",
4105
+ value: function registerUserActivity() {
4106
+ this.sendRequestToKeepSessionAlive();
4107
+ this.resetCheckForSessionExpiryTimeout(this.sessionTimeoutSeconds);
4108
+ }
4109
+ }, {
4110
+ key: "resetCheckForSessionExpiryTimeout",
4111
+ value: function resetCheckForSessionExpiryTimeout(intervalSeconds) {
4112
+ this.log("resetting session expiry timeout ".concat(intervalSeconds));
4113
+ clearTimeout(this.checkForSessionExpiryTimeout);
4114
+ this.checkForSessionExpiryTimeout = setTimeout(this.checkForSessionExpiry.bind(this), intervalSeconds * 1e3);
4115
+ }
4116
+ }, {
4117
+ key: "checkForSessionExpiry",
4118
+ value: function checkForSessionExpiry() {
4119
+ this.sendRequestToTestForSessionExpiry();
4120
+ this.resetCheckForSessionExpiryTimeout(this.throttlePeriodSeconds * 2);
4121
+ }
4122
+ }, {
4123
+ key: "sendRequestToKeepSessionAlive",
4124
+ value: function sendRequestToKeepSessionAlive() {
4125
+ this.ajaxGet(this.keepAlivePath);
4126
+ }
4127
+ }, {
4128
+ key: "sendRequestToTestForSessionExpiry",
4129
+ value: function sendRequestToTestForSessionExpiry() {
4130
+ this.log("checking for session expiry");
4131
+ this.ajaxGet(this.checkAlivePath);
4132
+ }
4133
+ }, {
4134
+ key: "ajaxGet",
4135
+ value: function ajaxGet(path) {
4136
+ Rails$1.ajax({
4137
+ type: "GET",
4138
+ url: path,
4139
+ dataType: "text",
4140
+ error: this.reloadPageIfAjaxRequestWasUnauthorised.bind(this)
4141
+ });
4142
+ }
4143
+ }, {
4144
+ key: "reloadPageIfAjaxRequestWasUnauthorised",
4145
+ value: function reloadPageIfAjaxRequestWasUnauthorised(responseText, status, xhr) {
4146
+ if (xhr.status == 401) {
4147
+ window.location.reload();
4148
+ this.sendLogoutMessageToAnyOpenTabs();
4149
+ }
4150
+ }
4151
+ }, {
4152
+ key: "addHandlersToMonitorUserActivity",
4153
+ value: function addHandlersToMonitorUserActivity() {
4154
+ document.addEventListener("click", this.throttledRegisterUserActivity.bind(this));
4155
+ document.addEventListener("keydown", this.throttledRegisterUserActivity.bind(this));
4156
+ window.addEventListener("resize", this.throttledRegisterUserActivity.bind(this));
4157
+ window.addEventListener("storage", this.storageChange.bind(this));
4158
+ }
4159
+ }, {
4160
+ key: "removeUserActivityHandlers",
4161
+ value: function removeUserActivityHandlers() {
4162
+ document.removeEventListener("click", this.throttledRegisterUserActivity.bind(this));
4163
+ document.removeEventListener("keydown", this.throttledRegisterUserActivity.bind(this));
4164
+ window.removeEventListener("resize", this.throttledRegisterUserActivity.bind(this));
4165
+ window.removeEventListener("storage", this.storageChange.bind(this));
4166
+ }
4167
+ }, {
4168
+ key: "logSettings",
4169
+ value: function logSettings() {
4170
+ if (this.debug) {
4171
+ this.log("keepAlivePath ".concat(this.keepAlivePath));
4172
+ this.log("checkAlivePath ".concat(this.checkAlivePath));
4173
+ this.log("loginPath ".concat(this.loginPath));
4174
+ this.log("sessionTimeoutSeconds ".concat(this.sessionTimeoutSeconds));
4175
+ this.log("throttlePeriodSeconds ".concat(this.throttlePeriodSeconds));
4176
+ }
4177
+ }
4178
+ }, {
4179
+ key: "log",
4180
+ value: function log(msg) {
4181
+ if (this.debug) {
4182
+ console.log(msg);
4183
+ }
4184
+ }
4185
+ }, {
4186
+ key: "storageChange",
4187
+ value: function storageChange(event) {
4188
+ if (event.key == "logout-event") {
4189
+ setTimeout(this.sendRequestToTestForSessionExpiry.bind(this), 2e3);
4190
+ }
4191
+ }
4192
+ }, {
4193
+ key: "onLoginPage",
4194
+ get: function get() {
4195
+ return window.location.pathname == this.loginPath;
4196
+ }
4197
+ }, {
4198
+ key: "debug",
4199
+ get: function get() {
4200
+ return this.data.get("debug");
4201
+ }
4202
+ } ]);
4203
+ return _default;
4204
+ }(Controller);
4205
+
4039
4206
  var application = Application.start();
4040
4207
 
4041
4208
  application.register("toggle", _default);
@@ -4052,4 +4219,6 @@ application.register("prescriptions", _default$5);
4052
4219
 
4053
4220
  application.register("charts", _default$6);
4054
4221
 
4222
+ application.register("session", _default$7);
4223
+
4055
4224
  window.Chartkick.use(window.Highcharts);
@@ -122,7 +122,7 @@
122
122
  }
123
123
  }
124
124
 
125
- &.wrapper_size_large {
125
+ &.wrapper_size_lg {
126
126
  > .wrapper__input {
127
127
  @media #{$medium-only} {
128
128
  @include grid-column(8, $last-column: false);
@@ -133,7 +133,7 @@
133
133
  }
134
134
  }
135
135
 
136
- &.wrapper_size_small {
136
+ &.wrapper_size_sm {
137
137
  > .wrapper__input {
138
138
  @media #{$medium-up} {
139
139
  @include grid-column(4, $last-column: false);
@@ -14,7 +14,7 @@ ul.f-dropdown#hd-prescription-options(data-dropdown-content aria-hidden="true")
14
14
  - else
15
15
  - prescriptions_to_give_on_hd.each do |prescription|
16
16
  = dropdown_btn_item title: prescription.drug_name,
17
- url: renalware.new_hd_prescription_administration_path(prescription),
17
+ url: renalware.new_hd_prescription_administration_path(prescription, format: :html),
18
18
  data: { "reveal-id" => "hd-prescription-administration-modal",
19
19
  "reveal-ajax" => "true" }
20
20
 
@@ -11,13 +11,13 @@ module Renalware
11
11
  after_action :verify_authorized
12
12
 
13
13
  # A note on ahoy tracking:
14
- # has_user_timed_out is defined on SessionTimeoutController
14
+ # check_session_expired is defined on SessionTimeoutController
15
15
  # using this in the that controller
16
- # skip_before_action :track_ahoy_visit, only: has_user_timed_out
16
+ # skip_before_action :track_ahoy_visit, only: check_session_expired
17
17
  # does not seem to work hence this global blacklist
18
18
 
19
19
  # rubocop:disable Rails/LexicallyScopedActionFilter
20
- after_action :track_action, except: [:has_user_timed_out, :status]
20
+ after_action :track_action, except: :status
21
21
  # rubocop:enable Rails/LexicallyScopedActionFilter
22
22
 
23
23
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # This controller exposes a has_user_timed_out action which is invoked via Ajax on a JavaScript
3
+ # This controller exposes a check_session_expired action which is invoked via Ajax on a JavaScript
4
4
  # timer and will cause the user to be redirected to the login page if their session has expired
5
5
  # due to a period of activity. The redirect happens because of a generic Ajax error handling
6
6
  # JavaScript - see ajax_errors.js - which causes the page to reload after any Ajax 401 error
@@ -11,35 +11,38 @@
11
11
  #
12
12
  module Renalware
13
13
  class SessionTimeoutController < BaseController
14
- prepend_before_action :skip_timeout, only: :has_user_timed_out
15
- skip_before_action :authenticate_user!, only: :has_user_timed_out
16
- skip_before_action :track_ahoy_visit, only: :has_user_timed_out
17
- protect_from_forgery except: [:has_user_timed_out, :reset_user_clock]
14
+ prepend_before_action :skip_timeout, only: :check_session_expired
15
+ skip_before_action :authenticate_user!, only: :check_session_expired
16
+ skip_before_action :track_ahoy_visit
17
+ protect_from_forgery only: []
18
+ after_action :track_action, only: []
18
19
 
19
20
  # Note this action will NOT update the session activity (thus keeping the session alive)
20
21
  # because we invoke #skip_timeout at the beginning of the filter chain.
21
22
  # We could return the amount of time remaining before the session expires like so
22
23
  # time_left = Devise.timeout_in - (Time.now - user_session["last_request_at"]).round
23
24
  # and display this to the user if required.
24
- # rubocop :disable Naming/PredicateName
25
- def has_user_timed_out
26
- skip_authorization
25
+ def check_session_expired
26
+ skip_authorization # pundit
27
27
  if referrer_is_a_devise_url? || !current_users_session_has_timed_out?
28
- head(:ok)
28
+ head :ok
29
29
  else
30
- flash[:notice] = "Your session timed due to inactivity. Please log in again."
31
30
  head :unauthorized
32
31
  end
33
32
  end
34
- # rubocop :enable Naming/PredicateName
35
33
 
36
- # A user could invoke this action to keep their session alive, by for example
37
- # clicking on a "Keep my session active" button which makes an ajax call to this action.
38
- # Note this will keep the session alive because we have not invoked skip_timeout before
39
- # action, so, like all actions on all controllers, the user's last_request_at time stamp is
34
+ # session_controller.js invoked this action to when there is user activity on the page
35
+ # to update the session window.
36
+ # Note this will keep the session alive because we have NOT invoked skip_timeout before the
37
+ # action, so, like all controller actions, the user's last_request_at time stamp is
40
38
  # updated in their session cookie.
41
- def reset_user_clock
42
- head :ok
39
+ def keep_session_alive
40
+ skip_authorization # pundit
41
+ if referrer_is_a_devise_url? || !current_users_session_has_timed_out?
42
+ head :ok
43
+ else
44
+ head :unauthorized
45
+ end
43
46
  end
44
47
 
45
48
  private
@@ -0,0 +1,223 @@
1
+
2
+ const Rails = window.Rails
3
+ const _ = window._
4
+ import { Controller } from "stimulus"
5
+
6
+ // This controller has 3 related functions
7
+ // - Keep a users session alive
8
+ // Keep the user's session alive if they are 'active' (there are keypresses,
9
+ // clicks or resize events on the same page) by sending a throttled ajax
10
+ // request to reset the session window which will prevent their session
11
+ // expiring and throwing them out when they are for example writing a long
12
+ // letter (which they would otherwise not finish before their session expires)
13
+ // - Auto logging-out a user after a period of inactivity
14
+ // Check after a period of intactivity to see if their session has expired.
15
+ // If it has then refresh the page which will redirect them to the login page.
16
+ // - Signalling to other open tabs when the user's session has expired or they
17
+ // have manually logged out - so that all tabs go to the login page at around
18
+ // the same time.
19
+ //
20
+ // Goals:
21
+ // - Performance and code clarity more important than having an accurate session
22
+ // window - if it is extended for a minute or two that is OK.
23
+ // - The server should always be the judge of whether the session has timed out
24
+ // - Query the server as little as possible - partly for performance and partly
25
+ // to avoid noise in the server logs
26
+ // - Keep event handler activity minimal to preserve CPU cycles - ie use
27
+ // throttle or debounce
28
+ //
29
+ // Possible enhancements:
30
+ // - After a period of inactivity, show a dialog asking if user wants to extend
31
+ // the session - this would involve starting a separate timer and displaying
32
+ // the countdown
33
+ //
34
+ // Scenarios to test:
35
+ // - Keypresses, clicks and window resizing - any of these should reset session
36
+ // and thus the user remains logged in as long as one of these events ocurrs
37
+ // within sessionTimeoutSeconds
38
+ // - User closes lid on laptop overnight and reopens in the morning - what is
39
+ // expected?
40
+ // - Network disconnected - what do we do?
41
+ // - user gets withing 10 seconds of session timeout and starts typing - session
42
+ // window shoud be reset
43
+ // - user has > 1 tab open and logs out of one - ideally it should log out of
44
+ // other tabs before too long. We do by setting a localStorage value to signal
45
+ // to other tabs
46
+ //
47
+ // Known issues:
48
+ // - user sitting on register page will keep polling checkAlivePath
49
+ // - if a user becomes active on a page within throttlePeriodSeconds of
50
+ // sessionTimeoutSeconds then there is no currently opportunity for
51
+ // throttledRegisterUserActivity to reset kick in a trump
52
+ // checkForSessionExpiryTimeout - so the session will log out. We might need
53
+ // an extra step before calling checkForSessionExpiry - a final chance to
54
+ // check if the user was
55
+ // active
56
+ // - Not quite sure if putting the data attribute config settings in the body
57
+ // tag is the right thing to do - perhaps should be in a config .js.erb
58
+ export default class extends Controller {
59
+ checkForSessionExpiryTimeout = null
60
+ userActivityDetected = false
61
+ checkAlivePath = null
62
+ keepAlivePath = null
63
+ loginPath = null
64
+ throttledRegisterUserActivity = null
65
+ sessionTimeoutSeconds = 0
66
+ defaultSessionTimeoutSeconds = 20 * 60 // 20 mins
67
+ throttlePeriodSeconds = 0
68
+ defaultThrottlePeriodSeconds = 20
69
+
70
+ initialize() {
71
+ this.throttlePeriodSeconds = parseInt(this.data.get("register-user-activity-after") || this.defaultThrottlePeriodSeconds)
72
+ this.sessionTimeoutSeconds = parseInt(this.data.get("timeout") || this.defaultSessionTimeoutSeconds)
73
+ this.sessionTimeoutSeconds += 10 // To allow for network roundtrips etc
74
+ this.checkAlivePath = this.data.get("check-alive-path")
75
+ this.loginPath = this.data.get("login-path")
76
+ this.keepAlivePath = this.data.get("keep-alive-path")
77
+ this.logSettings()
78
+
79
+ // Throttle the user activity callback because we only need to know about user activity
80
+ // only very occasionally, so that we can periodically tell there server the user was active.
81
+ // Here, even if there are hundreds of events (click, keypress etc) within throttlePeriodSeconds,
82
+ // our function is only called at most once in that period, when throttlePeriodSeconds has
83
+ // passed (since trailing = true). This suits is as we want to avoid making any call to the
84
+ // server unless the user has been on the page for at least throttlePeriodSeconds.
85
+ // See https://lodash.com/docs/#trottle
86
+ this.throttledRegisterUserActivity = _.throttle(
87
+ this.registerUserActivity.bind(this),
88
+ this.throttlePeriodSeconds * 1000,
89
+ { "leading": false, "trailing": true }
90
+ )
91
+ }
92
+
93
+ connect() {
94
+ if (this.onLoginPage) {
95
+ this.log("connect: onLoginPage - skipping session time")
96
+ } else {
97
+ this.addHandlersToMonitorUserActivity()
98
+ this.resetCheckForSessionExpiryTimeout(this.sessionTimeoutSeconds)
99
+ }
100
+ }
101
+
102
+ disconnect() {
103
+ if (!this.onLoginPage) {
104
+ this.removeUserActivityHandlers()
105
+ clearTimeout(this.checkForSessionExpiryTimeout)
106
+ }
107
+ }
108
+
109
+ sendLogoutMessageToAnyOpenTabs() {
110
+ window.localStorage.setItem("logout-event", "logout" + Math.random())
111
+ }
112
+
113
+ // Debounced event handler for key/click/resize
114
+ // If we come in there then the user has interacted with the page
115
+ // within throttlePeriodSeconds
116
+ registerUserActivity() {
117
+ this.sendRequestToKeepSessionAlive()
118
+ this.resetCheckForSessionExpiryTimeout(this.sessionTimeoutSeconds)
119
+ }
120
+
121
+ // Timeout handler for checking if the sesison has expired
122
+ resetCheckForSessionExpiryTimeout(intervalSeconds) {
123
+ this.log(`resetting session expiry timeout ${intervalSeconds}`)
124
+ clearTimeout(this.checkForSessionExpiryTimeout)
125
+ this.checkForSessionExpiryTimeout = setTimeout(
126
+ this.checkForSessionExpiry.bind(this),
127
+ intervalSeconds * 1000
128
+ )
129
+ }
130
+
131
+ // Here we really expect the session to have expired. In case it hasn't
132
+ // we reset the timeout to check again. We could reset the timeout to be
133
+ // sessionTimeoutSeconds, but if when we checked for expiry we had only just
134
+ // missed it, we will end up staying on this page (assuming the user is
135
+ // inactive) for nearly twice as long as we need to. So we set the timeout
136
+ // to be throttlePeriodSeconds * 2, which gives time for the
137
+ // throttledRegisterUserActivity handler to reset the session again if it
138
+ // fires.
139
+ checkForSessionExpiry() {
140
+ this.sendRequestToTestForSessionExpiry()
141
+ this.resetCheckForSessionExpiryTimeout(this.throttlePeriodSeconds * 2)
142
+ }
143
+
144
+ sendRequestToKeepSessionAlive() {
145
+ this.ajaxGet(this.keepAlivePath)
146
+ }
147
+
148
+ sendRequestToTestForSessionExpiry() {
149
+ this.log("checking for session expiry")
150
+ this.ajaxGet(this.checkAlivePath)
151
+ }
152
+
153
+ ajaxGet(path) {
154
+ Rails.ajax({
155
+ type: "GET",
156
+ url: path,
157
+ dataType: "text",
158
+ error: this.reloadPageIfAjaxRequestWasUnauthorised.bind(this)
159
+ })
160
+ }
161
+
162
+ reloadPageIfAjaxRequestWasUnauthorised(responseText, status, xhr) {
163
+ if (xhr.status == 401) {
164
+ window.location.reload()
165
+ this.sendLogoutMessageToAnyOpenTabs()
166
+ }
167
+ }
168
+
169
+ addHandlersToMonitorUserActivity() {
170
+ document.addEventListener("click", this.throttledRegisterUserActivity.bind(this))
171
+ document.addEventListener("keydown", this.throttledRegisterUserActivity.bind(this))
172
+ window.addEventListener("resize", this.throttledRegisterUserActivity.bind(this))
173
+ window.addEventListener("storage", this.storageChange.bind(this))
174
+ }
175
+
176
+ removeUserActivityHandlers() {
177
+ document.removeEventListener("click", this.throttledRegisterUserActivity.bind(this))
178
+ document.removeEventListener("keydown", this.throttledRegisterUserActivity.bind(this))
179
+ window.removeEventListener("resize", this.throttledRegisterUserActivity.bind(this))
180
+ window.removeEventListener("storage", this.storageChange.bind(this))
181
+ }
182
+
183
+ logSettings() {
184
+ if (this.debug) {
185
+ this.log(`keepAlivePath ${this.keepAlivePath}`)
186
+ this.log(`checkAlivePath ${this.checkAlivePath}`)
187
+ this.log(`loginPath ${this.loginPath}`)
188
+ this.log(`sessionTimeoutSeconds ${this.sessionTimeoutSeconds}`)
189
+ this.log(`throttlePeriodSeconds ${this.throttlePeriodSeconds}`)
190
+ }
191
+ }
192
+
193
+ log(msg) {
194
+ if (this.debug) {
195
+ console.log(msg)
196
+ }
197
+ }
198
+
199
+ // An event handler to watch for changes in the value of the local storage item called
200
+ // 'logged_in'. We use localStorage as a cross-tab communication protocol: when the user has
201
+ // logged out of one tab, this mechanism is used to signal to any other logged-in tabs that they
202
+ // should log themselves out.
203
+ // This applies in 2 circumstances:
204
+ // - the user has clicked the "Log Out" link in the navbar - the sendLogoutMessageToAnyOpenTabs()
205
+ // action defined above is called
206
+ // - our tab has timed out due to inactivity; other open tabs may not timeout for another few
207
+ // minutes (depending on the polling frequency etc) so we give them a nudge.
208
+ storageChange(event) {
209
+ if(event.key == "logout-event") {
210
+ setTimeout(this.sendRequestToTestForSessionExpiry.bind(this), 2000)
211
+ }
212
+ }
213
+
214
+ get onLoginPage() {
215
+ return window.location.pathname == this.loginPath
216
+ }
217
+
218
+ // If you add data-session-debug=1 then logging will be enabled
219
+ // This is evaluated each time we can add debugging into a running page
220
+ get debug() {
221
+ return this.data.get("debug")
222
+ }
223
+ }