// 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); });