Save settings on the server and sync them to every device
build-container / build (push) Successful in 2m2s
build-container / build (push) Successful in 2m2s
Preferences lived only in the browser, so clearing it, using another device or the separate iOS home-screen storage lost them. They are now saved in /data/settings.json (the audiobooks sign-in encrypted), loaded at start-up, and existing browser-only values are uploaded once. A failed save is reported instead of silent. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d3642a7736
commit
e22d26bf17
5 files changed
+87
-7
No files matched your search
+50
@@ -0,0 +1,50 @@
|
||||
// User preferences saved on the server (in the data folder) so they survive cleared browsers and follow you across devices.
|
||||
// Sensitive values (the audiobooks sign-in) are encrypted at rest with a key derived from the server secret.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const auth = require('./auth');
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data');
|
||||
const FILE = path.join(DATA_DIR, 'settings.json');
|
||||
|
||||
// Only these keys are accepted. (Per-device things like the Seafile sign-in token and the music queue stay in the browser.)
|
||||
const KEYS = new Set(['mail_hints', 'ab_creds', 'plex_lib', 'office_loc', 'cal_default', 'cal_view', 'cal_range', 'task_list', 'task_show_done', 'recents']);
|
||||
const SECRET = new Set(['ab_creds']);
|
||||
const MAX_VALUE = 20000; // bytes of JSON per key
|
||||
|
||||
const key = () => crypto.scryptSync(auth.secret, 'nehub-settings', 32);
|
||||
const seal = v => { const iv = crypto.randomBytes(12), c = crypto.createCipheriv('aes-256-gcm', key(), iv);
|
||||
const d = Buffer.concat([c.update(JSON.stringify(v), 'utf8'), c.final()]); return { _sealed: Buffer.concat([iv, c.getAuthTag(), d]).toString('base64') }; };
|
||||
const unseal = o => { try { const b = Buffer.from(o._sealed, 'base64'), d = crypto.createDecipheriv('aes-256-gcm', key(), b.subarray(0, 12));
|
||||
d.setAuthTag(b.subarray(12, 28)); return JSON.parse(Buffer.concat([d.update(b.subarray(28)), d.final()]).toString('utf8')); } catch { return undefined; } };
|
||||
|
||||
const load = () => { try { return JSON.parse(fs.readFileSync(FILE, 'utf8')); } catch { return {}; } };
|
||||
const save = o => { fs.mkdirSync(DATA_DIR, { recursive: true }); const tmp = FILE + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(o, null, 2), { mode: 0o600 }); fs.renameSync(tmp, FILE); }; // write-then-rename: a crash never leaves half a file
|
||||
const readBody = req => new Promise((ok, no) => { const c = []; let n = 0; req.on('data', d => { n += d.length; if (n > 100000) { no(new Error('Too large')); req.destroy(); } else c.push(d); }); req.on('end', () => ok(Buffer.concat(c).toString())); req.on('error', no); });
|
||||
const json = (res, code, obj) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(obj)); };
|
||||
|
||||
async function handle(req, res) {
|
||||
try {
|
||||
if (req.method === 'GET') {
|
||||
const raw = load(), values = {};
|
||||
for (const k of KEYS) { if (!(k in raw)) continue; const v = SECRET.has(k) && raw[k] && raw[k]._sealed ? unseal(raw[k]) : raw[k]; if (v !== undefined) values[k] = v; }
|
||||
return json(res, 200, { values });
|
||||
}
|
||||
if (req.method === 'PUT') {
|
||||
const { key: k, value } = JSON.parse(await readBody(req));
|
||||
if (!KEYS.has(k)) return json(res, 400, { error: 'Unknown setting' });
|
||||
const all = load();
|
||||
if (value === null || value === undefined) delete all[k];
|
||||
else {
|
||||
if (JSON.stringify(value).length > MAX_VALUE) return json(res, 400, { error: 'That setting is too large' });
|
||||
all[k] = SECRET.has(k) ? seal(value) : value;
|
||||
}
|
||||
save(all);
|
||||
return json(res, 200, { ok: true });
|
||||
}
|
||||
json(res, 405, { error: 'Method not allowed' });
|
||||
} catch (e) { json(res, 500, { error: e.message }); }
|
||||
}
|
||||
|
||||
module.exports = { handle, KEYS };
|
||||
Reference in new issue
Block a user