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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/renalware/components/session_timeout_redirect.js.erb +26 -21
- data/app/assets/javascripts/renalware/core.js.erb +2 -0
- data/app/assets/javascripts/renalware/rollup_compiled.js +169 -0
- data/app/assets/stylesheets/renalware/partials/_simple_form.scss +2 -2
- data/app/components/renalware/hd/administer_prescription_dropdown_component.html.slim +1 -1
- data/app/controllers/renalware/base_controller.rb +3 -3
- data/app/controllers/renalware/session_timeout_controller.rb +20 -17
- data/app/javascript/renalware/controllers/session_controller.js +223 -0
- data/app/javascript/renalware/index.js +2 -0
- data/app/models/renalware/accesses/assessment.rb +5 -1
- data/app/models/renalware/accesses/procedure.rb +4 -1
- data/app/models/renalware/accesses/profile.rb +4 -1
- data/app/models/renalware/clinical/body_composition.rb +4 -1
- data/app/models/renalware/clinical/dry_weight.rb +4 -1
- data/app/models/renalware/clinics/clinic_visit.rb +5 -1
- data/app/models/renalware/hd/preference_set.rb +4 -1
- data/app/models/renalware/hd/profile.rb +5 -2
- data/app/models/renalware/hd/session.rb +4 -1
- data/app/models/renalware/low_clearance/profile.rb +5 -2
- data/app/models/renalware/medications/prescription.rb +4 -2
- data/app/models/renalware/pathology/code_group.rb +3 -1
- data/app/models/renalware/pathology/code_group_membership.rb +4 -1
- data/app/models/renalware/patient.rb +4 -1
- data/app/models/renalware/patients/worry.rb +5 -1
- data/app/models/renalware/problems/problem.rb +5 -1
- data/app/models/renalware/renal/aki_alert.rb +4 -1
- data/app/models/renalware/renal/profile.rb +4 -1
- data/app/models/renalware/transplants/donation.rb +4 -2
- data/app/models/renalware/transplants/donor_followup.rb +4 -2
- data/app/models/renalware/transplants/donor_operation.rb +4 -2
- data/app/models/renalware/transplants/donor_workup.rb +4 -2
- data/app/models/renalware/transplants/recipient_followup.rb +5 -2
- data/app/models/renalware/transplants/recipient_operation.rb +5 -2
- data/app/models/renalware/transplants/recipient_workup.rb +5 -2
- data/app/models/renalware/transplants/registration.rb +5 -2
- data/app/models/renalware/transplants/rejection_episode.rb +1 -1
- data/app/models/renalware/ukrdc/{batch_number.rb → batch.rb} +1 -1
- data/app/models/renalware/ukrdc/create_encrypted_patient_xml_files.rb +7 -10
- data/app/models/renalware/ukrdc/create_patient_xml_file.rb +3 -4
- data/app/models/renalware/ukrdc/housekeeping/remove_old_export_archive_folders.rb +3 -3
- data/app/models/renalware/ukrdc/housekeeping/remove_stale_files.rb +2 -2
- data/app/models/renalware/ukrdc/incoming/import_surveys.rb +1 -2
- data/app/models/renalware/ukrdc/transmission_log.rb +3 -2
- data/app/presenters/renalware/user_session_presenter.rb +44 -0
- data/app/views/renalware/admissions/consults/_form.html.slim +19 -18
- data/app/views/renalware/hd/prescription_administrations/_form.html.slim +1 -1
- data/app/views/renalware/hd/prescription_administrations/_row.html.slim +1 -1
- data/app/views/renalware/hd/prescription_administrations/new.js.erb +1 -1
- data/app/views/renalware/hd/protocols/_protocol.pdf.slim +39 -38
- data/app/views/renalware/hd/scheduling/diary_slots/_slot.html.slim +2 -1
- data/app/views/renalware/hd/witnesses/_form.html.slim +2 -2
- data/app/views/renalware/letters/contacts/_contact.html.slim +1 -1
- data/app/views/renalware/letters/letters/index.html.slim +4 -0
- data/app/views/renalware/medications/prescriptions/index.html.slim +1 -1
- data/app/views/renalware/navigation/_sign_out.html.slim +5 -1
- data/app/views/renalware/patients/patients/show/_primary_care_physician.html.slim +1 -1
- data/app/views/renalware/transplants/wait_lists/show.html.slim +1 -1
- data/config/initializers/paper_trail.rb +1 -1
- data/config/initializers/simple_form_wrappers.rb +14 -9
- data/config/routes/system.rb +2 -1
- data/db/migrate/20200408131217_associate_batch_with_ukrdc_transmission_log.rb +15 -0
- data/db/seeds/seeds_helper.rb +8 -2
- data/lib/renalware/configuration.rb +6 -0
- data/lib/renalware/engine.rb +2 -1
- data/lib/renalware/version.rb +1 -1
- data/spec/support/shared_examples/supersedable_examples.rb +2 -2
- metadata +22 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6f402017b657476cbd3fbb3586210987688063fe3168b529fe9dca935b9ce11
|
4
|
+
data.tar.gz: 45f068499baf243025d6e1a09b9c0a685dc86d59dc07e3c2c6c0517b5c12adf5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
32
|
-
});
|
35
|
+
setInterval(window.sessionTimeoutCheck, (frequency_s * 1000));
|
36
|
+
});
|
37
|
+
<% end %>
|
@@ -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
|
-
&.
|
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
|
-
&.
|
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
|
-
#
|
14
|
+
# check_session_expired is defined on SessionTimeoutController
|
15
15
|
# using this in the that controller
|
16
|
-
# skip_before_action :track_ahoy_visit, only:
|
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:
|
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
|
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: :
|
15
|
-
skip_before_action :authenticate_user!, only: :
|
16
|
-
skip_before_action :track_ahoy_visit
|
17
|
-
protect_from_forgery
|
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
|
-
|
25
|
-
|
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
|
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
|
-
#
|
37
|
-
#
|
38
|
-
# Note this will keep the session alive because we have
|
39
|
-
# action, so, like all actions
|
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
|
42
|
-
|
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
|
+
}
|