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 db6abaefc7
commit 538a4b6603
14 changed files with 331 additions and 51 deletions
+10 -1
View File
@@ -152,15 +152,24 @@ func (s *Server) submitLogin(w http.ResponseWriter, r *http.Request) {
}
token := s.sessions.Create(admin.Username)
s.setSessionCookie(w, token)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
// setSessionCookie (re)issues the session cookie with a fresh Max-Age equal
// to the sliding idle window (plan B.1), so the browser-side expiry tracks
// whatever the database row was just set to — at login, and again whenever
// requireAuth extends an active session.
func (s *Server) setSessionCookie(w http.ResponseWriter, token string) {
http.SetCookie(w, &http.Cookie{
Name: s.sessionCookie(),
Value: token,
Path: "/",
MaxAge: s.sessions.MaxAge(),
HttpOnly: true,
Secure: s.cfg.CookieSecure,
SameSite: http.SameSiteLaxMode,
})
http.Redirect(w, r, "/", http.StatusSeeOther)
}
// handleLogout destroys the session and clears the cookie.