Persist admin sessions across redeploys
Sessions now load from and save to sessions.json in the data volume (0600, atomic write, expired entries pruned on save), so a redeploy no longer logs the admin out. Verified: session cookie survives a full server restart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -172,7 +172,23 @@ async function handleChat(req, res) {
|
|||||||
return json(res, 200, { reply: String(reply).trim().slice(0, 2000) });
|
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 }); }
|
} 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();
|
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) {
|
function ensureDataFile(name) {
|
||||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||||
@@ -293,10 +309,10 @@ async function handleApi(req,res,pathname){
|
|||||||
}
|
}
|
||||||
if(req.method==='POST'&&pathname==='/api/admin/login'){
|
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 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'){
|
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(pathname.startsWith('/api/admin/')&&!requireAdmin(req,res))return;
|
||||||
if(req.method==='GET'&&pathname==='/api/admin/matrix-tree'){
|
if(req.method==='GET'&&pathname==='/api/admin/matrix-tree'){
|
||||||
|
|||||||
Reference in New Issue
Block a user