/** * @file me.js * @description `/api/me` — the signed-in account's own UI preferences * (`user_prefs`), so they follow it to any browser and differ between the * accounts of one dashboard. Local mode is one account, `local`; server mode * keys a row `u` by the login cookie — the request never names an account. * Only the keys in `PREF_KEYS` are kept, each a string of at most 8 KB, and a * PUT merges into the row (`null` removes a key). Writes are same-origin. * Today the one key is `agenthub.chatUi` ("1" = Claude/Codex sessions open as * a chat, `PaneChat`). Same table, routes and semantics as the server-s3 work, * which adds its own keys to this list. * @author Nguyễn Ngọc Trí Vĩ */ const { Router } = require("express"); const { db } = require("../db"); const auth = require("../lib/auth"); const { sameOriginGuard } = require("../lib/same-origin"); const router = Router(); /** The keys an account's preferences are made of. */ const PREF_KEYS = ["agenthub.chatUi"]; const MAX_VALUE = 8 * 1024; /** `u` for a server-mode account, `local` in local mode, null when nobody is signed in. */ function userKey(req) { if (!auth.isServerMode()) return "local"; const user = req.user || auth.userFromRequest(req); return user ? `u${user.id}` : null; } const getRow = db.prepare("SELECT prefs, onboarded_at FROM user_prefs WHERE user_key = ?"); const upsert = db.prepare( `INSERT INTO user_prefs (user_key, prefs) VALUES (?, ?) ON CONFLICT(user_key) DO UPDATE SET prefs = excluded.prefs, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')` ); function read(key) { const row = getRow.get(key); let prefs = {}; try { prefs = row ? JSON.parse(row.prefs) : {}; } catch { /* a damaged row reads as empty */ } return { prefs, onboardedAt: row?.onboarded_at || null }; } const unauth = (res) => res.status(401).json({ error: { code: "EAUTH", message: "sign in first" } }); router.get("/prefs", (req, res) => { const key = userKey(req); if (!key) return unauth(res); res.json(read(key)); }); router.put("/prefs", sameOriginGuard, (req, res) => { const key = userKey(req); if (!key) return unauth(res); const patch = req.body?.prefs; if (!patch || typeof patch !== "object" || Array.isArray(patch)) { return res.status(400).json({ error: { code: "EBADPREFS", message: "prefs: an object" } }); } const { prefs } = read(key); for (const [k, v] of Object.entries(patch)) { if (!PREF_KEYS.includes(k)) { return res.status(400).json({ error: { code: "EBADPREFS", message: `unknown key ${k}` } }); } if (v === null) delete prefs[k]; else if (typeof v === "string" && v.length <= MAX_VALUE) prefs[k] = v; else return res .status(400) .json({ error: { code: "EBADPREFS", message: `${k}: a short string` } }); } upsert.run(key, JSON.stringify(prefs)); res.json(read(key)); }); module.exports = router; module.exports.PREF_KEYS = PREF_KEYS;