build-container / build (push) Successful in 4m42s
- Chores on the board are tappable; pick your name (PIN unless a child) and it happens as you - Up-for-grabs chores offer "I did it" or "Take it for later" - Number inputs (chore points, time limit) use the dark input style so they are readable - Tests and README updated Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
37 lines
5.7 KiB
JavaScript
37 lines
5.7 KiB
JavaScript
// Family devices: making one, the board, picking a profile (kids tap, others use a PIN), short sessions, and turning one off.
|
|
const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path'), crypto = require('crypto');
|
|
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-board-')); process.env.COOKIE_SECRET = 'x'.repeat(40);
|
|
const users = require('../users'), auth = require('../auth'), board = require('../board'), chores = require('../chores');
|
|
const ann = users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), kid = users.create({ name: 'Kai', role: 'user', tabs: ['chat', 'chores'], handle: 'Yg' }), bob = users.create({ name: 'Bob', role: 'user', tabs: ['chat', 'chores'], handle: 'Yw' });
|
|
users.update(kid.id, { kid: { on: true } });
|
|
const mac = v => crypto.createHmac('sha256', auth.secret).update(v).digest('base64url');
|
|
const session = u => { const exp = Date.now() + 864e5; return `hub_session=${u.id}.${u.epoch}.${exp}.${mac(`s${u.id}.${u.epoch}.${exp}`)}`; };
|
|
const hub = http.createServer((req, res) => req.url.startsWith('/api/chores') ? users.runAs(ann, () => chores.handle(req, res)) : board.handle(req, res)).listen(0, async () => {
|
|
const B = 'http://127.0.0.1:' + hub.address().port; let jar = {};
|
|
const call = async (p, o = {}, cookie) => { const r = await fetch(B + p, { ...o, headers: { ...(cookie ? { cookie } : {}), ...(o.headers || {}) } }); return { status: r.status, json: await r.json().catch(() => ({})), set: r.headers.getSetCookie ? r.headers.getSetCookie() : [] }; };
|
|
const post = (p, b, cookie) => call(p, { method: 'POST', body: JSON.stringify(b || {}) }, cookie);
|
|
try {
|
|
assert.equal((await call('/api/board')).status, 403, 'no board for a plain browser');
|
|
assert.equal((await post('/api/device/enable', {}, session(bob))).status, 403, 'only an admin makes a family device');
|
|
let r = await post('/api/device/enable', { name: 'Kitchen' }, session(ann)); assert.equal(r.status, 200); const dev = r.set.find(c => c.startsWith('hub_device=')).split(';')[0]; assert.ok(dev);
|
|
assert.ok(r.set.some(c => c.startsWith('hub_session=;') ), 'the admin is signed out of the device');
|
|
r = await call('/api/board?day=2026-10-26', {}, dev); assert.equal(r.status, 200); assert.deepEqual(r.json.people.map(p => [p.name, p.kid]), [['Ann', false], ['Kai', true], ['Bob', false]]);
|
|
users.update(bob.id, { board: false }); r = await call('/api/board?day=2026-10-26', {}, dev); assert.deepEqual(r.json.people.map(p => p.name), ['Ann', 'Kai'], 'only the people the admin picks are on the board'); assert.equal((await post('/api/board/switch', { uid: bob.id, pin: '2468' }, dev)).status, 404); users.update(bob.id, { board: true });
|
|
assert.equal((await call('/api/board', {}, 'hub_device=fake.sig')).status, 403, 'a forged device cookie is refused');
|
|
r = await post('/api/board/switch', { uid: kid.id }, dev); assert.equal(r.status, 200, JSON.stringify(r.json)); const sess = r.set.find(c => c.startsWith('hub_session=')); assert.ok(/Max-Age=1800/.test(sess), 'a short session on a family device'); assert.equal(auth.currentUser({ headers: { cookie: sess.split(';')[0] } }).id, kid.id);
|
|
const ch = (await post('/api/chores', { title: 'Sweep', repeat: 'daily', assign: 'anyone', points: 4 })).json, today = new Date().toISOString().slice(0, 10), act = (uid, action, pin) => post('/api/board/chore', { uid, chore: ch.id, action, pin, date: today }, dev);
|
|
let cb = (await call('/api/board?day=' + today, {}, dev)).json.chores.find(c => c.chore === ch.id); assert.equal(cb.claimable, true, 'the board shows the chore up for grabs');
|
|
assert.equal((await act(kid.id, 'claim')).status, 200, 'Kai takes it with one tap'); cb = (await call('/api/board?day=' + today, {}, dev)).json.chores.find(c => c.chore === ch.id); assert.equal(cb.claimed, true); assert.equal(cb.userName, 'Kai');
|
|
assert.equal((await act(bob.id, 'done')).status, 403, 'Bob has no PIN, so he cannot act as himself yet'); assert.equal((await act(kid.id, 'done')).status, 200); cb = (await call('/api/board?day=' + today, {}, dev)).json.chores.find(c => c.chore === ch.id); assert.equal(cb.done, true); assert.equal(cb.doneBy, 'Kai');
|
|
assert.equal((await act(kid.id, 'undo')).status, 200); assert.equal((await post('/api/board/chore', { uid: kid.id, chore: ch.id, action: 'done' }, '')).status, 403, 'only a family device can do this'); assert.equal((await act(kid.id, 'nope')).status, 400);
|
|
r = await post('/api/board/switch', { uid: bob.id }, dev); assert.equal(r.status, 403, 'an adult with no PIN cannot just tap'); assert.equal((await post('/api/board/switch', { uid: kid.id }, '')).status, 403, 'and only a family device can switch');
|
|
assert.equal((await post('/api/device/pin', { pin: '12' }, session(bob))).status, 400, 'a PIN is 4 to 8 digits'); assert.equal((await post('/api/device/pin', { pin: '2468' }, session(bob))).status, 200);
|
|
assert.equal((await post('/api/board/switch', { uid: bob.id, pin: '1111' }, dev)).status, 401); assert.equal((await post('/api/board/switch', { uid: bob.id, pin: '2468' }, dev)).status, 200);
|
|
for (let i = 0; i < 5; i++) await post('/api/board/switch', { uid: bob.id, pin: '0000' }, dev); assert.equal((await post('/api/board/switch', { uid: bob.id, pin: '2468' }, dev)).status, 429, 'five wrong tries lock it for a few minutes');
|
|
assert.equal((await call('/api/device', {}, session(ann) + '; ' + dev)).json.devices.length, 1);
|
|
r = await post('/api/device/remove', {}, session(ann) + '; ' + dev); assert.equal(r.status, 200); assert.equal((await call('/api/board', {}, dev)).status, 403, 'switched off: the old cookie no longer works');
|
|
console.log('board ok');
|
|
} catch (e) { console.error(e); process.exitCode = 1; }
|
|
hub.close(); setTimeout(() => process.exit(process.exitCode || 0), 50);
|
|
});
|