renalware-core 2.0.148 → 2.0.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }