From caee0b4cff623c846608d691fbc1578e93593a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=91=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=80=D0=B5=D0=B2?= Date: Sat, 6 Jun 2026 15:44:45 +0300 Subject: [PATCH] fix: persist admin sessions across restart --- admin-web/server.py | 27 ++++++++++++++++++++++++--- admin-web/static/app.js | 4 ++++ admin-web/static/index.html | 4 ++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/admin-web/server.py b/admin-web/server.py index 720ea84..a580b47 100644 --- a/admin-web/server.py +++ b/admin-web/server.py @@ -119,16 +119,37 @@ def public_host_for_notes() -> str: return HOST if HOST != "0.0.0.0" else "127.0.0.1" +def session_secret() -> bytes: + user, password = load_admin_credentials() + return f"{user}:{password}:{ADMIN_AUTH_FILE}".encode("utf-8") + + def make_session() -> str: - token = secrets.token_urlsafe(32) - SESSIONS[token] = time.time() + SESSION_TTL_SECONDS + nonce = secrets.token_urlsafe(24) + exp = int(time.time() + SESSION_TTL_SECONDS) + payload = f"{exp}.{nonce}" + sig = hmac.new(session_secret(), payload.encode("utf-8"), hashlib.sha256).hexdigest() + token = f"{payload}.{sig}" + SESSIONS[token] = exp return token def session_is_valid(token: str) -> bool: + if not token: + return False exp = SESSIONS.get(token) if not exp: - return False + parts = token.split(".") + if len(parts) != 3: + return False + exp_raw, nonce, sig = parts + if not exp_raw.isdigit() or not nonce: + return False + payload = f"{exp_raw}.{nonce}" + expected = hmac.new(session_secret(), payload.encode("utf-8"), hashlib.sha256).hexdigest() + if not hmac.compare_digest(sig, expected): + return False + exp = int(exp_raw) if exp < time.time(): SESSIONS.pop(token, None) return False diff --git a/admin-web/static/app.js b/admin-web/static/app.js index d533006..48ca8df 100644 --- a/admin-web/static/app.js +++ b/admin-web/static/app.js @@ -636,6 +636,10 @@ async function api(path, options = {}) { if (options.body && !headers["Content-Type"]) headers["Content-Type"] = "application/json"; const res = await fetch(path, { ...options, headers, credentials: "same-origin" }); const data = await res.json().catch(() => ({})); + if (res.status === 401) { + window.location.assign("/"); + throw new Error("unauthorized"); + } if (!res.ok || data.ok === false) throw new Error(data.error || `HTTP ${res.status}`); return data.data ?? data; } diff --git a/admin-web/static/index.html b/admin-web/static/index.html index 18a10f8..4926a72 100644 --- a/admin-web/static/index.html +++ b/admin-web/static/index.html @@ -11,7 +11,7 @@ document.documentElement.dataset.theme = theme; }()); - +
@@ -482,6 +482,6 @@
- +