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>
43 lines
2.9 KiB
JavaScript
43 lines
2.9 KiB
JavaScript
// The admin's tools for managing people: add someone, choose their tabs, give them a one-time invite code,
|
|
// sign them out everywhere, turn them off, or delete them and their data. Admin only (enforced by the caller).
|
|
const users = require('./users');
|
|
const auth = require('./auth');
|
|
const audit = require('./audit');
|
|
|
|
const { readBody } = require('./body');
|
|
const json = (res, code, obj) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(obj)); };
|
|
const row = u => ({ ...users.publicView(u), kid: u.kid || null, tabsChosen: u.tabs || [], passkeys: auth.passkeyCount(u.id) });
|
|
|
|
async function handle(req, res, me) {
|
|
try {
|
|
const p = new URL(req.url, 'http://x').pathname.replace(/\/$/, ''), parts = p.split('/'); // ['', 'api', 'users', id?, action?]
|
|
const id = parts[3], action = parts[4];
|
|
if (!id && req.method === 'GET') return json(res, 200, { users: users.all().map(row), tabs: users.TABS.map(t => ({ id: t, label: users.TAB_LABELS[t] })), invite: { minutes: auth.INVITE_MINUTES } });
|
|
if (!id && req.method === 'POST') {
|
|
const b = JSON.parse(await readBody(req) || '{}');
|
|
const u = users.create({ name: b.name, role: 'user', tabs: b.tabs });
|
|
audit.log('person added', { who: me.name, detail: u.name }, req);
|
|
return json(res, 200, { user: row(u), code: auth.newInvite(u.id), minutes: auth.INVITE_MINUTES });
|
|
}
|
|
const u = users.get(id); if (!u) return json(res, 404, { error: 'Person not found' });
|
|
if (action === 'invite' && req.method === 'POST') audit.log('invite created', { who: me.name, detail: u.name }, req);
|
|
if (action === 'invite' && req.method === 'POST') return json(res, 200, { code: auth.newInvite(u.id), minutes: auth.INVITE_MINUTES }); // for a new device or to recover access
|
|
if (!action && req.method === 'PUT') {
|
|
const b = JSON.parse(await readBody(req) || '{}');
|
|
if (u.id === me.id && b.disabled) return json(res, 400, { error: 'You cannot turn off your own account' });
|
|
audit.log('person changed', { who: me.name, detail: u.name + (b.disabled !== undefined ? (b.disabled ? ' (turned off)' : ' (turned on)') : '') + (b.signOutEverywhere ? ' (signed out everywhere)' : '') + (b.tabs ? ' (tabs)' : '') + (b.role ? ' (role)' : '') }, req);
|
|
users.update(id, { name: b.name, tabs: b.tabs, role: b.role, disabled: b.disabled, signOutEverywhere: b.signOutEverywhere, kid: b.kid, board: b.board, simple: b.simple });
|
|
return json(res, 200, { user: row(users.get(id)) });
|
|
}
|
|
if (!action && req.method === 'DELETE') {
|
|
if (u.id === me.id) return json(res, 400, { error: 'You cannot delete your own account' });
|
|
audit.log('person removed', { who: me.name, detail: u.name }, req);
|
|
users.remove(id); auth.removeCredsOf(id);
|
|
return json(res, 200, { ok: true });
|
|
}
|
|
json(res, 404, { error: 'Not found' });
|
|
} catch (e) { json(res, 400, { error: e.message }); }
|
|
}
|
|
|
|
module.exports = { handle };
|