build-container / build (push) Successful in 4m21s
- "Anyone" chores get Take it / Put back; a claimed chore is that person's for the day - Per-person "Show on the family board" switch controls board tiles and family chore progress - Tests and README updated Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
32 lines
4.4 KiB
JavaScript
32 lines
4.4 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');
|
|
const ann = users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), kid = users.create({ name: 'Kai', role: 'user', tabs: ['chat'], handle: 'Yg' }), bob = users.create({ name: 'Bob', role: 'user', tabs: ['chat'], 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) => 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);
|
|
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);
|
|
});
|