diff --git a/package-lock.json b/package-lock.json index 97f85b438b..ccd1c56980 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1767,9 +1767,9 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz", - "integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.7.tgz", + "integrity": "sha512-8XC0l0PwCbdg2Uc8zIIf6djNX3lYiz9GqQlC1LJ9WQvTYvcfP8IA9K2IKRnPm5tAX6X/+orF+WwKZ0doGcgJlg==", "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.0.3", @@ -2736,9 +2736,9 @@ } }, "babel-loader": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.4.tgz", - "integrity": "sha512-8dytA3gcvPPPv4Grjhnt8b5IIiTcq/zeXOPk4iTYI0SVXcsmuGg7JtBRDp8S9X+gJfhQ8ektjXZlDu1Bb33U8A==", + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz", + "integrity": "sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==", "dev": true, "requires": { "find-cache-dir": "^3.3.1", @@ -4714,9 +4714,9 @@ } }, "enhanced-resolve": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", - "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz", + "integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==", "dev": true, "requires": { "graceful-fs": "^4.2.4", @@ -7204,9 +7204,9 @@ } }, "loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true }, "loader-utils": { @@ -11286,9 +11286,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sass": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.50.0.tgz", - "integrity": "sha512-cLsD6MEZ5URXHStxApajEh7gW189kkjn4Rc8DQweMyF+o5HF5nfEz8QYLMlPsTOD88DknatTmBWkOcw5/LnJLQ==", + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.50.1.tgz", + "integrity": "sha512-noTnY41KnlW2A9P8sdwESpDmo+KBNkukI1i8+hOK3footBUcohNHtdOJbckp46XO95nuvcHDDZ+4tmOnpK3hjw==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0", @@ -14423,9 +14423,9 @@ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, "yargs": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.0.tgz", - "integrity": "sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA==", + "version": "17.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz", + "integrity": "sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==", "dev": true, "requires": { "cliui": "^7.0.2", diff --git a/requirements/base.in b/requirements/base.in index 2999c3bd89..15de1a0218 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -25,7 +25,7 @@ maykin-django-two-factor-auth phonenumbers django-localflavor django-privates -git+https://bitbucket.org/maykinmedia/django-digid-eherkenning.git@02ac61c42f6dd2f229ba9f0f687fa1a4160511be#egg=digid_eherkenning +django-digid-eherkenning django-cors-headers dj-rest-auth django-allauth diff --git a/requirements/base.txt b/requirements/base.txt index 733dd12bf4..8b92cb1635 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -40,25 +40,23 @@ cssselect2==0.4.1 # via weasyprint defusedxml==0.7.1 # via - # digid-eherkenning + # django-digid-eherkenning # odfpy # python3-openid diff-match-patch==20200713 # via django-import-export -digid-eherkenning @ git+https://bitbucket.org/maykinmedia/django-digid-eherkenning.git@02ac61c42f6dd2f229ba9f0f687fa1a4160511be - # via -r requirements/base.in dj-rest-auth==2.1.11 # via -r requirements/base.in django==3.2.12 # via # -r requirements/base.in - # digid-eherkenning # dj-rest-auth # django-allauth # django-appconf # django-axes # django-choices # django-cors-headers + # django-digid-eherkenning # django-extra-fields # django-filer # django-filter @@ -100,7 +98,7 @@ django-better-admin-arrayfield==1.4.2 django-choices==1.7.2 # via # -r requirements/base.in - # digid-eherkenning + # django-digid-eherkenning # mail-editor # zgw-consumers django-ckeditor==6.2.0 @@ -109,6 +107,8 @@ django-colorfield==0.4.5 # via -r requirements/base.in django-cors-headers==3.10.0 # via -r requirements/base.in +django-digid-eherkenning==0.3.1 + # via -r requirements/base.in django-elasticsearch-dsl==7.2.1 # via -r requirements/base.in django-extra-fields==3.0.2 @@ -198,7 +198,9 @@ fontawesomefree==6.1.1 fonttools[woff]==4.29.1 # via weasyprint furl==2.1.3 - # via -r requirements/base.in + # via + # -r requirements/base.in + # django-digid-eherkenning gemma-zds-client==1.0.1 # via zgw-consumers geographiclib==1.52 @@ -223,6 +225,7 @@ jsonschema==4.1.0 # via drf-spectacular lxml==4.6.3 # via + # django-digid-eherkenning # python3-saml # xmlsec mail-editor @ git+https://github.com/maykinmedia/mail-editor.git@0b4621b5c7f434586115b8e722af8940cfa70195 diff --git a/requirements/ci.txt b/requirements/ci.txt index 70daabed83..f835842991 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -76,7 +76,7 @@ defusedxml==0.7.1 # via # -c requirements/base.txt # -r requirements/base.txt - # digid-eherkenning + # django-digid-eherkenning # odfpy # python3-openid diff-match-patch==20200713 @@ -84,10 +84,6 @@ diff-match-patch==20200713 # -c requirements/base.txt # -r requirements/base.txt # django-import-export -digid-eherkenning @ git+https://bitbucket.org/maykinmedia/django-digid-eherkenning.git@02ac61c42f6dd2f229ba9f0f687fa1a4160511be - # via - # -c requirements/base.txt - # -r requirements/base.txt dj-rest-auth==2.1.11 # via # -c requirements/base.txt @@ -96,13 +92,13 @@ django==3.2.12 # via # -c requirements/base.txt # -r requirements/base.txt - # digid-eherkenning # dj-rest-auth # django-allauth # django-appconf # django-axes # django-choices # django-cors-headers + # django-digid-eherkenning # django-extra-fields # django-filer # django-filter @@ -158,7 +154,7 @@ django-choices==1.7.2 # via # -c requirements/base.txt # -r requirements/base.txt - # digid-eherkenning + # django-digid-eherkenning # mail-editor # zgw-consumers django-ckeditor==6.2.0 @@ -174,6 +170,10 @@ django-cors-headers==3.10.0 # via # -c requirements/base.txt # -r requirements/base.txt +django-digid-eherkenning==0.3.1 + # via + # -c requirements/base.txt + # -r requirements/base.txt django-elasticsearch-dsl==7.2.1 # via # -c requirements/base.txt @@ -360,6 +360,7 @@ furl==2.1.3 # via # -c requirements/base.txt # -r requirements/base.txt + # django-digid-eherkenning gemma-zds-client==1.0.1 # via # -c requirements/base.txt @@ -420,6 +421,7 @@ lxml==4.6.3 # via # -c requirements/base.txt # -r requirements/base.txt + # django-digid-eherkenning # pyquery # python3-saml # xmlsec diff --git a/requirements/dev.txt b/requirements/dev.txt index 6154e58347..aea1e842d1 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -96,7 +96,7 @@ defusedxml==0.7.1 # via # -c requirements/ci.txt # -r requirements/ci.txt - # digid-eherkenning + # django-digid-eherkenning # odfpy # python3-openid diff-match-patch==20200713 @@ -104,10 +104,6 @@ diff-match-patch==20200713 # -c requirements/ci.txt # -r requirements/ci.txt # django-import-export -digid-eherkenning @ git+https://bitbucket.org/maykinmedia/django-digid-eherkenning.git@02ac61c42f6dd2f229ba9f0f687fa1a4160511be - # via - # -c requirements/ci.txt - # -r requirements/ci.txt dj-rest-auth==2.1.11 # via # -c requirements/ci.txt @@ -117,7 +113,6 @@ django==3.2.12 # -c requirements/ci.txt # -r requirements/ci.txt # ddt-api-calls - # digid-eherkenning # dj-rest-auth # django-allauth # django-appconf @@ -125,6 +120,7 @@ django==3.2.12 # django-choices # django-cors-headers # django-debug-toolbar + # django-digid-eherkenning # django-extensions # django-extra-fields # django-filer @@ -181,7 +177,7 @@ django-choices==1.7.2 # via # -c requirements/ci.txt # -r requirements/ci.txt - # digid-eherkenning + # django-digid-eherkenning # mail-editor # zgw-consumers django-ckeditor==6.2.0 @@ -199,6 +195,10 @@ django-cors-headers==3.10.0 # -r requirements/ci.txt django-debug-toolbar==3.2.2 # via -r requirements/dev.in +django-digid-eherkenning==0.3.1 + # via + # -c requirements/ci.txt + # -r requirements/ci.txt django-elasticsearch-dsl==7.2.1 # via # -c requirements/ci.txt @@ -399,6 +399,7 @@ furl==2.1.3 # via # -c requirements/ci.txt # -r requirements/ci.txt + # django-digid-eherkenning gemma-zds-client==1.0.1 # via # -c requirements/ci.txt @@ -470,6 +471,7 @@ lxml==4.6.3 # via # -c requirements/ci.txt # -r requirements/ci.txt + # django-digid-eherkenning # pyquery # python3-saml # xmlsec diff --git a/src/open_inwoner/accounts/migrations/0030_message_file.py b/src/open_inwoner/accounts/migrations/0030_message_file.py index ba0d159f2d..e8d9d2b965 100644 --- a/src/open_inwoner/accounts/migrations/0030_message_file.py +++ b/src/open_inwoner/accounts/migrations/0030_message_file.py @@ -1,6 +1,7 @@ # Generated by Django 3.2.12 on 2022-03-07 15:33 from django.db import migrations, models + import privates.storages diff --git a/src/open_inwoner/accounts/migrations/0031_message_uuid.py b/src/open_inwoner/accounts/migrations/0031_message_uuid.py index 2be34fcecd..e9fecd3a4f 100644 --- a/src/open_inwoner/accounts/migrations/0031_message_uuid.py +++ b/src/open_inwoner/accounts/migrations/0031_message_uuid.py @@ -1,8 +1,9 @@ # Generated by Django 3.2.12 on 2022-03-14 10:54 -from django.db import migrations, models import uuid +from django.db import migrations, models + class Migration(migrations.Migration): diff --git a/src/open_inwoner/accounts/migrations/0033_alter_message_uuid.py b/src/open_inwoner/accounts/migrations/0033_alter_message_uuid.py index b498535894..c91031766c 100644 --- a/src/open_inwoner/accounts/migrations/0033_alter_message_uuid.py +++ b/src/open_inwoner/accounts/migrations/0033_alter_message_uuid.py @@ -1,8 +1,9 @@ # Generated by Django 3.2.12 on 2022-03-14 10:57 -from django.db import migrations, models import uuid +from django.db import migrations, models + class Migration(migrations.Migration): diff --git a/src/open_inwoner/conf/base.py b/src/open_inwoner/conf/base.py index 2575587b2d..8fbf8cbf9c 100644 --- a/src/open_inwoner/conf/base.py +++ b/src/open_inwoner/conf/base.py @@ -159,6 +159,7 @@ "open_inwoner.haalcentraal", "open_inwoner.openzaak", "open_inwoner.questionnaire", + "open_inwoner.extended_sessions", ] MIDDLEWARE = [ @@ -175,6 +176,7 @@ "hijack.middleware.HijackUserMiddleware", "django_otp.middleware.OTPMiddleware", "django.contrib.flatpages.middleware.FlatpageFallbackMiddleware", + "open_inwoner.extended_sessions.middleware.SessionTimeoutMiddleware", ] ROOT_URLCONF = "open_inwoner.urls" @@ -358,6 +360,9 @@ SESSION_COOKIE_NAME = "open_inwoner_sessionid" SESSION_ENGINE = "django.contrib.sessions.backends.cache" +ADMIN_SESSION_COOKIE_AGE = 86400 +SESSION_WARN_DELTA = 60 # Warn 1 minute before end of session. +SESSION_COOKIE_AGE = 900 # Set to 15 minutes LOGIN_REDIRECT_URL = reverse_lazy("root") LOGOUT_REDIRECT_URL = reverse_lazy("root") diff --git a/src/open_inwoner/extended_sessions/__init__.py b/src/open_inwoner/extended_sessions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/open_inwoner/extended_sessions/middleware.py b/src/open_inwoner/extended_sessions/middleware.py new file mode 100644 index 0000000000..9b4ac62672 --- /dev/null +++ b/src/open_inwoner/extended_sessions/middleware.py @@ -0,0 +1,27 @@ +from datetime import timedelta + +from django.conf import settings + +SESSION_EXPIRES_IN_HEADER = "X-Session-Expires-In" + + +class SessionTimeoutMiddleware: + """ + Allows us to set the expiry time of the session based on what + is configured in our GlobalConfiguration + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + timeout = ( + settings.ADMIN_SESSION_COOKIE_AGE + if request.user.is_staff + else settings.SESSION_COOKIE_AGE + ) + # https://docs.djangoproject.com/en/2.2/topics/http/sessions/#django.contrib.sessions.backends.base.SessionBase.set_expiry + request.session.set_expiry(timeout) + response = self.get_response(request) + response[SESSION_EXPIRES_IN_HEADER] = timeout + return response diff --git a/src/open_inwoner/extended_sessions/templates/sessions/session_timeout.html b/src/open_inwoner/extended_sessions/templates/sessions/session_timeout.html new file mode 100644 index 0000000000..0434ccbdc0 --- /dev/null +++ b/src/open_inwoner/extended_sessions/templates/sessions/session_timeout.html @@ -0,0 +1,10 @@ +{% load l10n %} + +{% if user.is_authenticated %} +
+
+{% endif %} diff --git a/src/open_inwoner/extended_sessions/templatetags/__init__.py b/src/open_inwoner/extended_sessions/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/open_inwoner/extended_sessions/templatetags/session_tags.py b/src/open_inwoner/extended_sessions/templatetags/session_tags.py new file mode 100644 index 0000000000..af073e9582 --- /dev/null +++ b/src/open_inwoner/extended_sessions/templatetags/session_tags.py @@ -0,0 +1,17 @@ +from django import template +from django.conf import settings + +register = template.Library() + + +@register.inclusion_tag("sessions/session_timeout.html", takes_context=True) +def session_timeout(context): + session = context["request"].session + context.update( + { + "expiry_age": session.get_expiry_age() + + 1, # Add a second to make sure the session has expired. + "warn_time": session.get_expiry_age() - settings.SESSION_WARN_DELTA, + } + ) + return context diff --git a/src/open_inwoner/extended_sessions/tests/__init__.py b/src/open_inwoner/extended_sessions/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/open_inwoner/extended_sessions/tests/test_extended_session.py b/src/open_inwoner/extended_sessions/tests/test_extended_session.py new file mode 100644 index 0000000000..f5489e9fb6 --- /dev/null +++ b/src/open_inwoner/extended_sessions/tests/test_extended_session.py @@ -0,0 +1,34 @@ +from django.conf import settings +from django.urls import reverse + +from django_webtest import WebTest + +from open_inwoner.accounts.tests.factories import UserFactory + + +class SessionBackendTest(WebTest): + def setUp(self): + self.url = reverse("sessions:restart-session") + + def test_default_session_length_when_not_logged_in(self): + response = self.app.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertEqual(self.app.session._get_session().get("_session_expiry"), 900) + + def test_default_session_length_when_regular_user(self): + user = UserFactory(is_staff=False) + response = self.app.get(self.url, user=user) + self.assertEqual(response.status_code, 200) + self.assertEqual( + self.app.session._get_session().get("_session_expiry"), + settings.SESSION_COOKIE_AGE, + ) + + def test_extended_session_length_when_staff_user(self): + user = UserFactory(is_staff=True) + response = self.app.get(self.url, user=user) + self.assertEqual(response.status_code, 200) + self.assertEqual( + self.app.session._get_session().get("_session_expiry"), + settings.ADMIN_SESSION_COOKIE_AGE, + ) diff --git a/src/open_inwoner/extended_sessions/tests/test_views.py b/src/open_inwoner/extended_sessions/tests/test_views.py new file mode 100644 index 0000000000..add8d7a45d --- /dev/null +++ b/src/open_inwoner/extended_sessions/tests/test_views.py @@ -0,0 +1,22 @@ +from django.test import TestCase +from django.urls import reverse + +from django_webtest import WebTest + +from open_inwoner.accounts.tests.factories import UserFactory + + +class ViewTest(WebTest): + def setUp(self): + self.url = reverse("sessions:restart-session") + + def test_when_not_logged_in(self): + response = self.app.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"") + + def test_when_logged_in(self): + user = UserFactory() + response = self.app.get(self.url, user=user) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b"restarted") diff --git a/src/open_inwoner/extended_sessions/urls.py b/src/open_inwoner/extended_sessions/urls.py new file mode 100644 index 0000000000..461b9bd711 --- /dev/null +++ b/src/open_inwoner/extended_sessions/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from .views import RestartSessionView + +app_name = "sessions" +urlpatterns = [ + path("restart/", RestartSessionView.as_view(), name="restart-session"), +] diff --git a/src/open_inwoner/extended_sessions/views.py b/src/open_inwoner/extended_sessions/views.py new file mode 100644 index 0000000000..997c5287c0 --- /dev/null +++ b/src/open_inwoner/extended_sessions/views.py @@ -0,0 +1,23 @@ +from django.http import HttpResponse +from django.views.generic import View + + +class RestartSessionView(View): + """ + This view is used from the SessionTimeout Javascript + class to determine if the user is logged in or not. + + This used to be done by doing a XMLHttpRequest to '/' and + checking the 'responseURL' if it was redirected to the + login page. However, Internet Explorer does not support + this method. + """ + + http_method_names = [ + "get", + ] + + def get(self, request): + if request.user.is_authenticated: + return HttpResponse("restarted") + return HttpResponse() diff --git a/src/open_inwoner/js/components/index.js b/src/open_inwoner/js/components/index.js index 15bcf55ecc..393972521e 100644 --- a/src/open_inwoner/js/components/index.js +++ b/src/open_inwoner/js/components/index.js @@ -17,3 +17,4 @@ import './notifications' import './preview' import './search' import './toggle' +import './session' diff --git a/src/open_inwoner/js/components/session/index.js b/src/open_inwoner/js/components/session/index.js new file mode 100644 index 0000000000..b2199a0ee1 --- /dev/null +++ b/src/open_inwoner/js/components/session/index.js @@ -0,0 +1,167 @@ +import Swal from 'sweetalert2' + +function currentTime() { + return Math.floor(Date.now() / 1000) +} + +/* + * Show a message to the user 1 minutes before the session timeout + */ +class SessionTimeout { + constructor() { + this.element = document.getElementById('session-timeout') + if (!this.element) { + return + } + + this.configureTimeout() + this.configureActivityCheck() + } + + configureTimeout() { + console.log('Session started.') + this.userActive = null + + this.setDataset() + + // When the session has been restarted, there can be lingering timeouts. + clearTimeout(this.warningTimeout) + clearTimeout(this.expiredTimeout) + + this.warningTimeout = setTimeout( + this.showWarningModal.bind(this), + this.warnTime * 1000 + ) + this.expiredTimeout = setTimeout( + this.showExpiredModal, + (this.expiryAge + 1) * 1000 + ) + } + + setDataset() { + console.log('setDataset') + this.expiryAge = parseInt(this.element.dataset.expiryAge) + this.warnTime = parseInt(this.element.dataset.warnTime) + console.log('this.expiryAge', this.expiryAge) + console.log('this.warnTime', this.warnTime) + } + + showWarningModal() { + console.log('showWarningModal') + if (this.userActive) { + this.restartSession() + return + } + + this.configureModal( + 'Uw sessie verloopt spoedig', + 'Klik op de knop "Doorgaan" om verder te gaan met de huidige sessie.', + 'Doorgaan', + this.restartSession + ) + } + + showExpiredModal() { + this.configureModal( + 'Uw sessie is verlopen', + 'Klik op de knop "Doorgaan" om opnieuw in te loggen.', + 'Doorgaan', + this.reloadPage + ) + } + + /* + * Restart the HTTP session by doing an HTTP-request. + */ + restartSession() { + fetch('/sessions/restart/') + .then((response) => response.text()) + .then((data) => this.resetWarnTime(data)) + } + + reloadPage() { + window.location.reload() + } + + configureModal(title, bodyText, buttonText, callback) { + Swal.fire({ + title: title, + html: bodyText, + showConfirmButton: true, + confirmButtonText: buttonText, + }).then(callback.bind(this)) + } + + configureActivityCheck() { + /* + * Based on if there is user activity restart the HTTP session. To avoid + * the user being logged out while still entering data into a form. + */ + + /* + * Note: 'keyup' does not account for tablet/phone users (and probably other devices). + */ + document.addEventListener('keyup', this.setUserActivity) + + this.configureActivityTimeout() + } + + configureActivityTimeout() { + clearTimeout(this.activityRestart) + + this.activityRestart = setTimeout( + this.restartNoActivity.bind(this), + 30 * 1000 + ) + } + + restartNoActivity() { + this.configureActivityTimeout() + + /* + * After a session restart we register if a user was active (in setUserActivity). + * If this was the case, and if it was more than a minute ago, restart the session. + * + * If the user is still active, we restart the session right before it + * expires in showWarningModal. + * + * This latest step is done to avoid sending a lot of requests to the server. + */ + + if (this.userActive === null) { + console.log('No user activity after the session started.') + return + } + + let latestActivity = currentTime() - this.userActive + console.log( + `Latest activity after the session started was ${latestActivity} seconds ago` + ) + + if (latestActivity >= 60) { + this.restartSession() + } + } + + setUserActivity() { + this.userActive = currentTime() + console.log(`Registered user activity, timestamp: ${this.userActive}`) + } + + resetWarnTime(response) { + // If we are redirected to a login page. + if (response !== 'restarted') { + clearTimeout(this.expiredTimeout) + + // Wait for the current modal to close and then open the expired one. + // Ensure that the event is unbound after, to avoid an additional pop up before reload. + this.showExpiredModal() + } else { + this.configureTimeout() + } + } +} + +document.addEventListener('DOMContentLoaded', function () { + const s = new SessionTimeout() +}) diff --git a/src/open_inwoner/pdc/migrations/0026_auto_20220312_1054.py b/src/open_inwoner/pdc/migrations/0026_auto_20220312_1054.py index 88cceb084b..170ec180cd 100644 --- a/src/open_inwoner/pdc/migrations/0026_auto_20220312_1054.py +++ b/src/open_inwoner/pdc/migrations/0026_auto_20220312_1054.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.12 on 2022-03-12 09:54 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/src/open_inwoner/templates/master.html b/src/open_inwoner/templates/master.html index 45e9486259..d7e7a0c2a4 100644 --- a/src/open_inwoner/templates/master.html +++ b/src/open_inwoner/templates/master.html @@ -1,4 +1,4 @@ -{% load static i18n header_tags card_tags footer_tags button_tags notification_tags anchor_menu_tags view_breadcrumbs utils %} +{% load static i18n header_tags card_tags footer_tags button_tags notification_tags anchor_menu_tags view_breadcrumbs utils session_tags %} @@ -61,6 +61,7 @@ {% endblock main_outer %} {% footer logo_url=site_logo footer_texts=configurable_text.footer %} + {% session_timeout %} diff --git a/src/open_inwoner/urls.py b/src/open_inwoner/urls.py index d969036a01..539720e029 100644 --- a/src/open_inwoner/urls.py +++ b/src/open_inwoner/urls.py @@ -59,6 +59,10 @@ "questionnaire/", include("open_inwoner.questionnaire.urls", namespace="questionnaire"), ), + path( + "sessions/", + include("open_inwoner.extended_sessions.urls", namespace="sessions"), + ), path("faq/", FAQView.as_view(), name="general_faq"), path("", include("open_inwoner.pdc.urls", namespace="pdc")), path("", include("open_inwoner.search.urls", namespace="search")),