fix(admin): stop logging out active long-form work; raise idle timeout to 4h

The admin idle timeout (30 min) was refreshed only by navigations and HTMX
requests. During long encoding sessions on an open form there are none, so
an actively-typing admin was logged out mid-work after ~30-45 min.

Add an activity-driven keepalive:
- /admin/session-keepalive.php: 204 when authenticated (refreshes
  admin_last_activity via AdminAuth::isAuthenticated()), 401 otherwise.
- admin-session-keepalive.js: marks activity only on real user input
  (pointer/keyboard/input/scroll/wheel/touch/focus) and pings at most once
  per 5 min while the tab is visible. A genuinely idle tab never pings, so
  the idle timeout still applies.

Raise the idle window 30 min -> 4 h: for a single-/few-admin back-office
whose main workflow is data entry, 30 min still kicked admins who stepped
away mid-form. With the keepalive in place, 4 h means "no interaction at
all", not "no navigation". Absolute timeout stays 12 h.

Also fix session ID rotation, which never fired: it used
`$absolute % IDLE_TIMEOUT_SECONDS === 0`, i.e. required a request to land
exactly on a multiple of the interval relative to login time. Replaced with
an explicit admin_last_rotation timestamp and a ROTATION_INTERVAL_SECONDS
(30 min) constant decoupled from the idle timeout, so raising the idle
window does not widen the fixation/replay window.

Refactor AdminAuth::enforceSessionTimeout() to return bool instead of
redirecting/exiting, so the keepalive endpoint can report 401 cleanly
rather than letting fetch follow a redirect to the login page.

Smoke test (just smoke-session-keepalive) covers activity refresh, 2 h idle
accepted, rotation firing, idle rejection+destruction, and unauthenticated
rejection. Docs updated.
This commit is contained in:
Pontoporeia
2026-09-18 16:26:49 +02:00
parent 25c5133086
commit 554ba3ee8d
10 changed files with 317 additions and 24 deletions
+9
View File
@@ -54,6 +54,15 @@
- [x] #rewrite-cc2r-checkbox-label Rewrite CC2r checkbox label to 'J'adhère au Collective Commitment to Reuse (CC2r)' (italic, both Libre and Interne branches)
- [x] #fix-admin-contenus-page [!high] Fix admin contenus page slowdown: langues/mots-clés fragments ship 2.4MB HTML (737 tag rows + 217 lang rows, ~1960 inline SVG icons + per-row CSRF forms). Implement htmx infinite-scroll 'load more' (paged fragments via limit/offset) for both tables.
- [x] #reduce-contenus-initial-page [!high] Reduce contenus initial page size from 100 to 25 rows per table (payload ~160KB total vs ~534KB)
- [x] #add-admin-session-keepalive Add admin session keepalive endpoint /admin/session-keepalive.php (204, gated, refreshes admin_last_activity)
- [x] #add-admin-session-keepalive-js-activity-driven-throttled Add admin-session-keepalive.js: activity-driven throttled ping (5min), visibility-aware, wired into admin-entry
- [x] #update-docs-security-md-to-document Update docs/security.md to document activity-driven keepalive vs navigation-only timeout
- [x] #add-test-for-session Add test for session keepalive endpoint auth gating + activity refresh
- [x] #raise-admin-idle-timeout Raise admin idle timeout 30min -> 4h for long-form encoding; keep absolute 12h
- [x] #fix-never-firing-session-id Fix never-firing session ID rotation (modulo on absolute age); use admin_last_rotation + interval
- [x] #decouple-rotation-interval-30min Decouple rotation interval (30min) from idle timeout constant
- [x] #update-docs-security-md-deploy-server-sh Update docs/security.md + deploy-server.sh comment for new idle value
- [x] #extend-smoke-test-for Extend smoke test for rotation and 4h idle boundary
## Deferred / Blocked
- [ ] #just-setup-backs-a [!medium] just setup backs a stale setup-dev.sh (clones php-live-reload, legacy admin/data/ dirs) — needs rewrite or removal