Parent controls for children, name-tile sign-in, restricted Learn search, simple mode
build-container / build (push) Successful in 3m52s
build-container / build (push) Successful in 3m52s
- Sign-in page shows name tiles on remembered devices; tap a name, then PIN or passkey - Music and Movies are separate on the server; Plex can be locked to a child's managed profile - Activity log, weekly summary and alerts to parents; per-area and weekend limits - Requests from children (Ask button) answered in Settings; bonus screen time from rewards - Restricted Learn search (Simple English Wikipedia, blocked words) replaces web search for children - Simple mode (big-button home) for young children or grandparents - Child usage kept in memory instead of read from disk on every request - Tests and README updated Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aac6f6efc2
commit
301e8017d9
34 files changed
+466
-51
No files matched your search
@@ -52,7 +52,7 @@ async function board(day) {
|
||||
}
|
||||
|
||||
async function handle(req, res) {
|
||||
const u = new URL(req.url, 'http://x'), p = u.pathname, me = auth.currentUser(req), dev = auth.deviceOf(req);
|
||||
const u = new URL(req.url, 'http://x'), p = u.pathname, me = auth.currentUser(req), anyDev = auth.deviceOf(req), dev = auth.deviceKind(req) === 'family' ? anyDev : null; // (dev: a family device; anyDev: any remembered device)
|
||||
try {
|
||||
if (p === '/api/board' && req.method === 'GET') { if (!dev) throw fail(403, 'This is not a family device'); const day = u.searchParams.get('day'); return json(res, 200, await board(ymdOk(day) ? day : new Date().toISOString().slice(0, 10))); }
|
||||
if (p === '/api/board/chore' && req.method === 'POST') { // a person on the shared screen takes a chore, ticks it off, or undoes it, without signing in
|
||||
@@ -74,13 +74,20 @@ async function handle(req, res) {
|
||||
identify(t, b.pin, req); const r = Readable.from([Buffer.from(JSON.stringify(plan[3]))]); r.url = plan[2]; r.method = 'POST'; r.headers = {};
|
||||
return users.runAs(t, () => plan[1].handle(r, res)); // (as that person, so that person's own rules apply)
|
||||
}
|
||||
if (p === '/api/login/people' && req.method === 'GET') { // the sign-in page's name tiles: only on a remembered device (family or a person's own)
|
||||
if (!anyDev) throw fail(403, 'This device is not remembered'); return json(res, 200, { people: users.all().filter(u => !u.disabled).map((u, i) => ({ id: u.id, name: u.name, kid: isKid(u), pin: hasPin(u.id), pinLen: (db()[u.id] || {}).n || 0, passkey: auth.passkeyCount(u.id) > 0, color: palette[i % palette.length] })) });
|
||||
}
|
||||
if (p === '/api/login/pin' && req.method === 'POST') { // sign in from a name tile with a PIN (a child with no PIN just taps)
|
||||
if (!anyDev) throw fail(403, 'This device is not remembered'); const b = JSON.parse(await readBody(req, 5e3) || '{}'), t = users.get(String(b.uid));
|
||||
if (!t || t.disabled) throw fail(404, 'Person not found'); identify(t, b.pin, req); auth.startSession(req, res, t); audit.log('sign-in', { who: t.name, detail: 'name tile' }, req); return json(res, 200, { ok: true });
|
||||
}
|
||||
if (p === '/api/board/switch' && req.method === 'POST') {
|
||||
if (!dev) throw fail(403, 'This is not a family device'); const b = JSON.parse(await readBody(req, 5e3) || '{}'), t = users.get(String(b.uid));
|
||||
if (!t || !onBoard(t)) throw fail(404, 'Person not found');
|
||||
identify(t, b.pin, req);
|
||||
auth.startSession(req, res, t); audit.log('family device: picked a profile', { who: t.name }, req); return json(res, 200, { ok: true });
|
||||
}
|
||||
if (p === '/api/device' && req.method === 'GET') { if (!me) throw fail(401, 'Not signed in'); return json(res, 200, { device: !!dev, idleMin: idleMin(), idleChoices: IDLE_CHOICES, pin: hasPin(me.id), devices: me.role === 'admin' ? devices.load() : undefined }); }
|
||||
if (p === '/api/device' && req.method === 'GET') { if (!me) throw fail(401, 'Not signed in'); return json(res, 200, { device: !!dev, trusted: auth.deviceKind(req) === 'trusted', idleMin: idleMin(), idleChoices: IDLE_CHOICES, pin: hasPin(me.id), devices: me.role === 'admin' ? devices.load() : undefined }); }
|
||||
if (p === '/api/device/pin' && (req.method === 'POST' || req.method === 'DELETE')) {
|
||||
if (!me) throw fail(401, 'Not signed in'); if (req.method === 'DELETE') { delete db()[me.id]; savePins(); return json(res, 200, { pin: false }); }
|
||||
const b = JSON.parse(await readBody(req, 5e3) || '{}'); setPin(me.id, b.pin); return json(res, 200, { pin: true });
|
||||
@@ -93,8 +100,12 @@ async function handle(req, res) {
|
||||
if (!me || me.role !== 'admin') throw fail(403, 'Only an admin can do this'); const b = JSON.parse(await readBody(req, 5e3) || '{}'); auth.enrollDevice(req, res, b.name, me.name); audit.log('family device added', { who: me.name, detail: String(b.name || '').slice(0, 40) }, req);
|
||||
res.setHeader('Set-Cookie', [].concat(res.getHeader('Set-Cookie') || [], 'hub_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0')); return json(res, 200, { ok: true });
|
||||
}
|
||||
if (p === '/api/device/trust' && req.method === 'POST') { // a person remembers their own device, so the sign-in page shows name tiles on it
|
||||
if (!me) throw fail(401, 'Not signed in'); if (anyDev) return json(res, 200, { ok: true }); auth.enrollDevice(req, res, '', me.name, 'trusted', me.id); audit.log('device remembered', { who: me.name }, req); return json(res, 200, { ok: true });
|
||||
}
|
||||
if (p === '/api/device/remove' && req.method === 'POST') { // switch one off: this screen, or another from the list
|
||||
if (!me || me.role !== 'admin') throw fail(403, 'Only an admin can do this'); const b = JSON.parse(await readBody(req, 5e3) || '{}');
|
||||
if (!me) throw fail(401, 'Not signed in'); const b = JSON.parse(await readBody(req, 5e3) || '{}'), here = anyDev && devices.get(anyDev);
|
||||
if (me.role !== 'admin' && !(here && here.kind === 'trusted' && here.byId === me.id && !b.id)) throw fail(403, 'Only an admin can do this');
|
||||
if (b.id) devices.remove(String(b.id)); else auth.forgetDevice(req, res); audit.log('family device removed', { who: me.name }, req); return json(res, 200, { ok: true });
|
||||
}
|
||||
return json(res, 404, { error: 'Not found' });
|
||||
|
||||
Reference in new issue
Block a user