Skip to content

Commit

Permalink
[ERP-877]
Browse files Browse the repository at this point in the history
* Add option to "Continue working" which resets the session refresh functionality
* Add option to "Logout"
* Display message to user when session has expired
* Add option to login again with a routing link back to the clinical form the user was filling before their session expired.
  • Loading branch information
ppettitau committed Dec 6, 2023
1 parent 304538e commit 329793d
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 66 deletions.
3 changes: 3 additions & 0 deletions rdrf/rdrf/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,9 @@
# Frontend session renewal
SESSION_REFRESH_MAX_RETRIES = env.get('session_refresh_max_retries', 5)
SESSION_REFRESH_LEAD_TIME = env.get('session_refresh_lead_time', 120)
# How far in advance before the end of the session should the user be warned their session's expiring?
# The value should be less than SESSION_COOKIE_AGE and greater than or equal to SESSION_REFRESH_LEAD_TIME
SESSION_EXPIRY_WARNING_LEAD_TIME = env.get('session_expiry_warning_lead_time', 300)

# Quicklinks settings
QUICKLINKS_CLASS = 'rdrf.forms.navigation.quick_links.QuickLinks'
Expand Down
201 changes: 155 additions & 46 deletions rdrf/rdrf/templates/rdrf_cdes/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
{% load static %}
{% load i18n admin_urls static admin_modify %}
{% load translate %}
{% load session_refresh_interval %}
{% load session_refresh_lead_time %}


{% block extrastyle %}
Expand Down Expand Up @@ -544,76 +542,187 @@
<div id="sessionTimeoutModal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans "Your session is about to expire." %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
</div>
<div class="modal-body">
<p>{% trans "Your session will expire in " %}<strong id="sessionExpiryTimeout">X seconds</strong>.</p>
<p>{% trans "Press 'OK' and save your changes or reload the page to continue using this session." %}</p>
<p data-session-state="expiring">{% trans "Your session will expire in " %}<strong id="sessionExpiryTimeout">X seconds</strong>.</p>
<p data-session-state="expired" class="d-none alert alert-danger">Your session has expired.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">{% trans "OK" %}</button>
<div data-session-state="expiring">
<button id="logoutBtn" type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Logout" %}</button>
<button id="continueWorkingBtn" type="button" class="btn btn-primary" data-bs-dismiss="modal">{% trans "Continue working" %}</button>
</div>
<div data-session-state="expired" class="d-none">
<button id="goToLoginBtn" type="button" class="btn btn-primary" data-bs-dismiss="modal">{% trans "Go to Login Page" %}</button>
</div>
</div>
</div>
</div>
</div>


<script>
$(document).ready(function () {

$(":input").not(':input[type=checkbox], :input[type=radio], :input[type=button], :input[type=submit], :input[type=reset]').addClass("form-control");
$("textarea").addClass("form-control");
$("select").addClass("form-select");
$("label[for*='-clear']").removeClass();
function ModalSessionNotifier() {

const SessionState = {
Expiring: "expiring",
Expired: "expired"
}

const $sessionTimeoutModal = $('#sessionTimeoutModal');
const $sessionExpiredComponents = $(`[data-session-state="${SessionState.Expired}"]`);
const $sessionExpiringComponents = $(`[data-session-state="${SessionState.Expiring}"]`);
const sessionTimeoutModal = new bootstrap.Modal($sessionTimeoutModal, {backdrop: "static"});
let countdownInterval;

const showSessionTimeoutModal = () => {
const updateSessionTimeoutMessage = (secondsLeft) => {
$sessionTimeoutModal.find("#sessionExpiryTimeout").text(`${secondsLeft} ${gettext("seconds")}`);
function updateSessionTimeoutMessage(secondsLeft) {
const ONE_MINUTE_IN_SECONDS = 60;
let unit = "seconds";
let time = secondsLeft;

if (secondsLeft >= ONE_MINUTE_IN_SECONDS) {
time = Math.round(secondsLeft / ONE_MINUTE_IN_SECONDS);
unit = time > 1 ? "minutes" : "minute";
}

const logout = () => {
window.location.href = '{% url "logout" %}?next={% url "login_router" %}'
$sessionTimeoutModal.find("#sessionExpiryTimeout").text(`${time} ${gettext(unit)}`);
}

function displayComponents(sessionState) {
switch (sessionState) {
case SessionState.Expiring: {
$sessionExpiredComponents.addClass("d-none");
$sessionExpiringComponents.removeClass("d-none");
break;
}
case SessionState.Expired: {
$sessionExpiredComponents.removeClass("d-none");
$sessionExpiringComponents.addClass("d-none");
console.log('showing expired.');
$('.modal-backdrop').css("opacity", ".9");
break;
}
}
}

function clearCountdownTimer() {
if (countdownInterval) clearInterval(countdownInterval);
}

let sessionExpiryCountdownInterval;
let sessionExpiryIntervalDelayInSeconds = 30;
let sessionExpiresInSeconds = parseInt("{% session_refresh_lead_time %}");

const sessionExpiryCountdown = () => {
if (sessionExpiresInSeconds >= 0) {
updateSessionTimeoutMessage(sessionExpiresInSeconds);
if (sessionExpiresInSeconds === 30) {
clearInterval(sessionExpiryCountdownInterval);
sessionExpiryIntervalDelayInSeconds = 10;
sessionExpiryCountdownInterval = setInterval(sessionExpiryCountdown, sessionExpiryIntervalDelayInSeconds * 1000);
function show(secondsLeftInSession) {
let secondsUntilNextCountdownUpdate = 30;

function countdownTimer() {
if (secondsLeftInSession > 0) {
updateSessionTimeoutMessage(secondsLeftInSession);
if (secondsLeftInSession === 30) {
clearCountdownTimer();
secondsUntilNextCountdownUpdate = 10;
countdownInterval = setInterval(countdownTimer, secondsUntilNextCountdownUpdate * 1000);
}
secondsLeftInSession -= secondsUntilNextCountdownUpdate;
} else {
clearCountdownTimer();
displayComponents(SessionState.Expired)
}
sessionExpiresInSeconds -= sessionExpiryIntervalDelayInSeconds;
} else {
setTimeout( ()=>{ logout() }, 10 * 1000);
}
}

sessionExpiryCountdownInterval = setInterval(sessionExpiryCountdown, sessionExpiryIntervalDelayInSeconds * 1000);
sessionExpiryCountdown();
sessionTimeoutModal.show();
countdownInterval = setInterval(countdownTimer, secondsUntilNextCountdownUpdate * 1000);
countdownTimer();
displayComponents(SessionState.Expiring);
sessionTimeoutModal.show();
}

const session_refresh_interval = setInterval( function() {
$.get("{% url 'session_refresh' %}", function( data, status, jqXHR ) {
if (data && !data.success) {
clearInterval(session_refresh_interval);
return {
clearCountdownTimer,
show
}
}

showSessionTimeoutModal();
}
}).fail(function() {
alert("{% trans "Error while refreshing session. Please check your connection" %}");

function SessionManager(sessionNotifier) {

const maxSessionAge = parseInt("{{ session_info.max_session_age }}");
const sessionWarningLeadTime = parseInt("{{ session_info.warning_lead_time }}");
const sessionRefreshLeadTime = parseInt("{{ session_info.refresh_lead_time }}");
const secondsUntilNextSessionRefresh = maxSessionAge - sessionRefreshLeadTime;
const secondsUntilExpiryWarning = maxSessionAge - sessionWarningLeadTime;
let sessionRefreshInterval;
let restartCount = 0;

function keepAlive(forcedRefresh = false) {
const data = {};
if (forcedRefresh) data['forced_refresh'] = true;
return $.ajax({
url: "{% url 'session_refresh' %}",
type: "get",
data
})
.fail(function() { alert("{% trans "Error while refreshing session. Please check your connection" %}");
});
}, {% session_refresh_interval %});
}

function restart() {
sessionNotifier.clearCountdownTimer();

const sessionRefresh = (forcedRefresh = false) => {
keepAlive(forcedRefresh).then(function ( data, status, jqXHR ) {
if (data && !data.success) {
clearInterval(sessionRefreshInterval);
setTimeout(() => { sessionNotifier.show(sessionWarningLeadTime) }, secondsUntilExpiryWarning * 1000);
}
});
}

if (restartCount > 0) {
sessionRefresh(true);
}
restartCount += 1;

sessionRefreshInterval = setInterval(sessionRefresh, secondsUntilNextSessionRefresh * 1000);
}

function logout() {
window.location.href = `{% url 'logout' %}?next=${(window.location.pathname)}`;
}

function goToLoginPage() {
window.location.href = `{% url 'two_factor:login' %}?next=${(window.location.pathname)}`;
}

// initialise session manager
restart();

return {
restart,
logout,
goToLoginPage
}

}

$(document).ready(function () {

$(":input").not(':input[type=checkbox], :input[type=radio], :input[type=button], :input[type=submit], :input[type=reset]').addClass("form-control");
$("textarea").addClass("form-control");
$("select").addClass("form-select");
$("label[for*='-clear']").removeClass();

const sessionNotifier = ModalSessionNotifier();
const sessionManager = SessionManager(sessionNotifier);

$("#continueWorkingBtn").on("click", function() {
sessionManager.restart();
});

$("#logoutBtn").on("click", function() {
sessionManager.logout();
});

$("#goToLoginBtn").on("click", function() {
sessionManager.goToLoginPage();
});

});
</script>

Expand Down
10 changes: 0 additions & 10 deletions rdrf/rdrf/templatetags/session_refresh_interval.py

This file was deleted.

9 changes: 0 additions & 9 deletions rdrf/rdrf/templatetags/session_refresh_lead_time.py

This file was deleted.

3 changes: 3 additions & 0 deletions rdrf/rdrf/views/form_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,9 @@ def get(self, request, registry_code, form_id, patient_id, context_id=None):
self.set_code_generator_data(context, empty_stubs=conditional_rendering_disabled)

context["selected_version_name"] = selected_version_name
context["session_info"] = {'max_session_age': settings.SESSION_COOKIE_AGE,
'warning_lead_time': settings.SESSION_EXPIRY_WARNING_LEAD_TIME,
'refresh_lead_time': settings.SESSION_REFRESH_LEAD_TIME}

xray_recorder.end_subsegment()

Expand Down
4 changes: 3 additions & 1 deletion rdrf/rdrf/views/session_refresh_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
@require_GET
def session_refresh(request):
num_retries = request.session.get('num_retries')
if num_retries is None:
forced_refresh = request.GET.get('forced_refresh', '')
reset_num_retries = forced_refresh.lower() == 'true'
if num_retries is None or reset_num_retries:
request.session['num_retries'] = settings.SESSION_REFRESH_MAX_RETRIES
num_retries = settings.SESSION_REFRESH_MAX_RETRIES
elif num_retries > 0:
Expand Down

0 comments on commit 329793d

Please sign in to comment.