Always revalidate the app's files, tell open pages when a newer version exists, and log why a session is refused
build-container / build (push) Successful in 1m58s

An open page kept running the old version after a deploy (the single page never reloads), so the new
People screens did not appear. Static files are now sent with no-cache + ETag (304 when unchanged), icons
are cached for a week, /api/config reports a build stamp, and the app shows "A new version is available -
Reload" when it comes back into view or every 10 minutes. The service worker cache name is bumped.

The server also logs, at most once a minute per reason, why it refused a session cookie (unknown account,
turned off, signed out everywhere, bad signature, expired) without printing the cookie.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-20 16:55:44 -04:00
1 parent 2d17ee9939
commit 12a1e50a0e
6 files changed
+57 -9

No files matched your search

+18 -5
View File
@@ -42,18 +42,31 @@ const isHttps = req => (req.headers['x-forwarded-proto'] || '') === 'https' || (
const safeEq = (a, b) => a.length === b.length && crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
/** Session cookie: uid.epoch.expiry.signature. (Older single-user cookies, expiry.signature, count as the admin.) */
const rejectedAt = new Map();
const whyRejected = (req, why) => { // explain a refused session in the log (at most once a minute per reason), without printing the cookie
const k = why, last = rejectedAt.get(k) || 0; if (Date.now() - last < 60e3) return; rejectedAt.set(k, Date.now());
console.warn(`[auth] session refused: ${why} (${req.method} ${req.url.split('?')[0]})`);
};
function currentUser(req) {
const parts = String(cookies(req).hub_session || '').split('.');
const raw = cookies(req).hub_session;
if (!raw) return null;
const parts = String(raw).split('.');
let user = null, exp;
if (parts.length === 4) {
const [uid, epoch, e, sig] = parts; exp = Number(e);
const u = users.get(uid);
if (u && !u.disabled && String(u.epoch) === epoch && safeEq(sig, mac(`s${uid}.${epoch}.${e}`))) user = u;
if (!u) whyRejected(req, 'unknown account ' + uid);
else if (u.disabled) whyRejected(req, 'account is turned off');
else if (String(u.epoch) !== epoch) whyRejected(req, `signed out everywhere (cookie epoch ${epoch}, account epoch ${u.epoch})`);
else if (!safeEq(sig, mac(`s${uid}.${epoch}.${e}`))) whyRejected(req, 'bad signature (the cookie secret changed, or the cookie was altered)');
else user = u;
} else if (parts.length === 2) {
const [e, sig] = parts; exp = Number(e);
if (safeEq(sig, mac('s' + e))) user = users.firstAdmin();
}
return user && exp > Date.now() ? user : null;
if (safeEq(sig, mac('s' + e))) { user = users.firstAdmin(); if (!user) whyRejected(req, 'old-style cookie but no admin account exists'); }
else whyRejected(req, 'bad signature on an old-style cookie (the cookie secret changed?)');
} else whyRejected(req, 'cookie is not in a known format');
if (user && !(exp > Date.now())) { whyRejected(req, 'session expired'); return null; }
return user;
}
const isAuthed = req => !!currentUser(req);