diff --git a/backend/workflows/context_processors.py b/backend/workflows/context_processors.py index 6fac856..450bd35 100644 --- a/backend/workflows/context_processors.py +++ b/backend/workflows/context_processors.py @@ -19,5 +19,12 @@ def role_context(request): ) else: context.update({'header_notifications': [], 'header_unread_notification_count': 0}) - context.update({'static_asset_version': settings.STATIC_ASSET_VERSION}) + context.update( + { + 'static_asset_version': settings.STATIC_ASSET_VERSION, + 'session_idle_timeout_seconds': settings.SESSION_IDLE_TIMEOUT_SECONDS, + 'session_reauth_timeout_seconds': settings.SENSITIVE_ACTION_REAUTH_SECONDS, + 'force_branded_error_pages': settings.FORCE_BRANDED_ERROR_PAGES, + } + ) return context diff --git a/backend/workflows/middleware.py b/backend/workflows/middleware.py index 65e2f64..367a4ca 100644 --- a/backend/workflows/middleware.py +++ b/backend/workflows/middleware.py @@ -127,6 +127,7 @@ class RateLimitMiddleware: class AuthSessionHardeningMiddleware: EXEMPT_PREFIXES = ( '/healthz/', + '/session/keepalive/', '/i18n/', '/accounts/login/', '/accounts/logout/', diff --git a/backend/workflows/static/workflows/js/session_warning.js b/backend/workflows/static/workflows/js/session_warning.js new file mode 100644 index 0000000..736acd4 --- /dev/null +++ b/backend/workflows/static/workflows/js/session_warning.js @@ -0,0 +1,82 @@ +(function () { + const config = window.WorkdockSessionConfig; + if (!config || !config.idleTimeoutSeconds) return; + + const warningLeadSeconds = Math.min(300, Math.max(60, Math.floor(config.idleTimeoutSeconds / 6))); + const modal = document.getElementById("app-session-warning-modal"); + const countdown = document.getElementById("app-session-warning-countdown"); + const extendButton = document.getElementById("app-session-warning-extend"); + if (!modal || !countdown || !extendButton) return; + + let lastConfirmedAt = Date.now(); + let warningVisible = false; + let keepaliveInFlight = false; + + function getCsrfToken() { + const cookie = document.cookie + .split(";") + .map((item) => item.trim()) + .find((item) => item.startsWith("csrftoken=")); + return cookie ? decodeURIComponent(cookie.split("=")[1]) : ""; + } + + function hideWarning() { + if (!warningVisible) return; + modal.hidden = true; + modal.setAttribute("aria-hidden", "true"); + warningVisible = false; + } + + function showWarning(secondsLeft) { + countdown.textContent = `Noch etwa ${secondsLeft} Sekunden bis zur automatischen Abmeldung.`; + if (warningVisible) return; + modal.hidden = false; + modal.setAttribute("aria-hidden", "false"); + warningVisible = true; + } + + async function sendKeepalive() { + if (keepaliveInFlight) return; + keepaliveInFlight = true; + try { + const response = await fetch(config.keepaliveUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": getCsrfToken(), + "X-Requested-With": "XMLHttpRequest", + }, + credentials: "same-origin", + body: JSON.stringify({ keepalive: true }), + }); + if (!response.ok) { + window.location.href = config.loginUrl; + return; + } + lastConfirmedAt = Date.now(); + hideWarning(); + } catch (_error) { + window.location.href = config.loginUrl; + } finally { + keepaliveInFlight = false; + } + } + + function tick() { + const elapsedSeconds = Math.floor((Date.now() - lastConfirmedAt) / 1000); + const secondsLeft = config.idleTimeoutSeconds - elapsedSeconds; + if (secondsLeft <= 0) { + window.location.href = config.loginUrl; + return; + } + if (secondsLeft <= warningLeadSeconds) { + showWarning(secondsLeft); + } + } + + extendButton.addEventListener("click", function () { + sendKeepalive(); + }); + + setInterval(tick, 1000); +})(); diff --git a/backend/workflows/templates/workflows/base_shell.html b/backend/workflows/templates/workflows/base_shell.html index 54e2e6b..8d4bf40 100644 --- a/backend/workflows/templates/workflows/base_shell.html +++ b/backend/workflows/templates/workflows/base_shell.html @@ -14,6 +14,16 @@ {% block extra_head %}{% endblock %}
+ {% if request.user.is_authenticated %} + + {% endif %} {% block pre_shell %}{% endblock %} {% if portal_trial_enabled %} + + {% block extra_scripts %}{% endblock %}