feat: implement B.1 — persist login sessions in SQLite with sliding idle timeout

Sessions move from an in-memory map (absolute 12h TTL) to a `sessions`
table (migration 0002), storing only the SHA-256 of the token. Expiry is
now a sliding idle window (PANEL_SESSION_IDLE_DAYS, default 7, no
absolute cap), extended at most once an hour and never by the
monitoring screens' background polling (GET + HX-Request), so a
forgotten open tab doesn't keep a session alive indefinitely. A login
now survives a container restart or redeploy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 23:21:49 +03:00
parent 750a65d5ee
commit 82ec287ba1
14 changed files with 331 additions and 51 deletions
+9 -1
View File
@@ -59,6 +59,10 @@ type Config struct {
// only for the send log (the journal-milter fails open).
OpenDKIMSocket string
JournalSocket string
// SessionIdleDays is the sliding inactivity window after which a login
// session expires (env PANEL_SESSION_IDLE_DAYS, plan B.1). Non-positive
// falls back to the 7-day default.
SessionIdleDays int
}
// Server is the panel HTTP application.
@@ -87,13 +91,17 @@ func New(st *store.Store, domains *domain.Service, apps *app.Service, cfg Config
if err != nil {
return nil, err
}
idleDays := cfg.SessionIdleDays
if idleDays <= 0 {
idleDays = 7
}
s := &Server{
store: st,
domains: domains,
apps: apps,
cfg: cfg,
tmpl: tmpl,
sessions: newSessionStore(),
sessions: newSessionStore(st, time.Duration(idleDays)*24*time.Hour),
// Published-DNS checks for the status page and the domain pages. The
// checker caches its own results, so page views do not each pay for a
// round of lookups (phase 13).