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.
- 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
|
+
}
|