Family devices: shared-screen family board with a who-is-this profile picker
build-container / build (push) Successful in 4m30s

- Admin can make a screen a family device; it opens on a family board
- Name tiles with KID labels; kids tap, others enter a PIN (lockout after 5 wrong tries)
- Name label with Switch user, auto-return to the board after 5 idle minutes
- Short 30-minute sessions on family devices
- Settings section for PIN and turning a device on or off
- Test, README and roadmap updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-26 12:58:23 -04:00
1 parent 058f410a58
commit eef1adaa73
21 files changed
+300 -14

No files matched your search

+12 -3
View File
@@ -72,13 +72,22 @@ function currentUser(req) {
}
const isAuthed = req => !!currentUser(req);
const sessionCookie = (req, user) => { const exp = Date.now() + LIFETIME;
return `hub_session=${user.id}.${user.epoch}.${exp}.${mac(`s${user.id}.${user.epoch}.${exp}`)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_DAYS * 86400}${isHttps(req) ? '; Secure' : ''}`; };
// A family device (a shared screen) is signed in for only a short while after each tap, so whoever used it last is not left signed in.
const KIOSK = 30 * 60e3;
const devices = require('./devices');
const deviceOf = req => { const [id, sig] = String(cookies(req).hub_device || '').split('.'); return id && sig && safeEq(sig, mac('d' + id)) && devices.has(id) ? id : null; };
const lifeFor = req => deviceOf(req) ? KIOSK : LIFETIME;
const sessionCookie = (req, user, life = lifeFor(req)) => { const exp = Date.now() + life;
return `hub_session=${user.id}.${user.epoch}.${exp}.${mac(`s${user.id}.${user.epoch}.${exp}`)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${Math.round(life / 1000)}${isHttps(req) ? '; Secure' : ''}`; };
const deviceCookie = (req, id) => `hub_device=${id}.${mac('d' + id)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${3650 * 86400}${isHttps(req) ? '; Secure' : ''}`;
const enrollDevice = (req, res, name, by) => { const d = devices.add(name, by); addCookie(res, deviceCookie(req, d.id)); return d; };
const forgetDevice = (req, res) => { const id = deviceOf(req); if (id) devices.remove(id); addCookie(res, 'hub_device=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0'); };
// Add a Set-Cookie without clobbering ones other code already set on this response.
const addCookie = (res, c) => { const cur = res.getHeader('Set-Cookie'); res.setHeader('Set-Cookie', cur ? [].concat(cur, c) : c); };
function startSession(req, res, user) { addCookie(res, sessionCookie(req, user)); }
/** Renew the cookie when it is more than a day old (and upgrade old-style cookies), so an active session never runs out. */
function refresh(req, res, user) {
if (deviceOf(req)) { const e = Number(String(cookies(req).hub_session || '').split('.')[2]); if (!(e - Date.now() > KIOSK - 60e3)) addCookie(res, sessionCookie(req, user, KIOSK)); return; }
const parts = String(cookies(req).hub_session || '').split('.');
const exp = Number(parts.length === 4 ? parts[2] : parts[0]);
if (parts.length !== 4 || exp - Date.now() < LIFETIME - 864e5) addCookie(res, sessionCookie(req, user));
@@ -248,4 +257,4 @@ async function handle(req, res) {
const passkeyCount = uid => credsOf(uid).length;
function removeCredsOf(uid) { store.creds = store.creds.filter(c => c.uid !== uid); save(); delete recovery[uid]; saveRecovery(); }
module.exports = { handle, currentUser, isAuthed, refresh, addCookie, secret, SESSION_DAYS, newInvite, INVITE_MINUTES, passkeyCount, removeCredsOf };
module.exports = { deviceOf, enrollDevice, forgetDevice, startSession, handle, currentUser, isAuthed, refresh, addCookie, secret, SESSION_DAYS, newInvite, INVITE_MINUTES, passkeyCount, removeCredsOf };