diff --git a/server.js b/server.js index 38e7977..79383bc 100644 --- a/server.js +++ b/server.js @@ -172,7 +172,23 @@ async function handleChat(req, res) { return json(res, 200, { reply: String(reply).trim().slice(0, 2000) }); } catch (e) { console.error('openrouter error', e.message); return json(res, 200, { fallback: true }); } } +// Sessions persist in the data volume so redeploys stop logging the admin out. +const SESSIONS_FILE = path.join(DATA_DIR, 'sessions.json'); const sessions = new Map(); +try { + const saved = JSON.parse(fs.readFileSync(SESSIONS_FILE, 'utf8')); + const now = Date.now(); + for (const [t, s] of Object.entries(saved)) if (s && s.expires > now) sessions.set(t, s); +} catch (e) {} +function saveSessions() { + try { + const now = Date.now(); + for (const [t, s] of sessions) if (s.expires <= now) sessions.delete(t); + const tmp = SESSIONS_FILE + '.tmp'; + fs.writeFileSync(tmp, JSON.stringify(Object.fromEntries(sessions)), { mode: 0o600 }); + fs.renameSync(tmp, SESSIONS_FILE); + } catch (e) { console.error('session save failed', e.message); } +} function ensureDataFile(name) { fs.mkdirSync(DATA_DIR, { recursive: true }); @@ -293,10 +309,10 @@ async function handleApi(req,res,pathname){ } if(req.method==='POST'&&pathname==='/api/admin/login'){ const b=await bodyJson(req).catch(e=>null);if(!b)return json(res,400,{error:'Invalid request'});if(typeof b.password!=='string'||b.password!==ADMIN_PASSWORD)return json(res,401,{error:'Invalid password'}); - const token=crypto.randomBytes(32).toString('hex');sessions.set(token,{expires:Date.now()+SESSION_TTL});const cookie=`ctb.sid=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL/1000}${IS_PROD?'; Secure':''}`;return json(res,200,{ok:true},{'Set-Cookie':cookie}); + const token=crypto.randomBytes(32).toString('hex');sessions.set(token,{expires:Date.now()+SESSION_TTL});saveSessions();const cookie=`ctb.sid=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL/1000}${IS_PROD?'; Secure':''}`;return json(res,200,{ok:true},{'Set-Cookie':cookie}); } if(req.method==='POST'&&pathname==='/api/admin/logout'){ - const s=getSession(req);if(s)sessions.delete(s.token);return json(res,200,{ok:true},{'Set-Cookie':'ctb.sid=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0'}); + const s=getSession(req);if(s){sessions.delete(s.token);saveSessions();}return json(res,200,{ok:true},{'Set-Cookie':'ctb.sid=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0'}); } if(pathname.startsWith('/api/admin/')&&!requireAdmin(req,res))return; if(req.method==='GET'&&pathname==='/api/admin/matrix-tree'){