Audit log (admin Activity list) and passkey recovery codes

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-22 07:53:41 -04:00
1 parent c8399ccaa7
commit 561b6181a0
8 files changed
+101 -14

No files matched your search

+13
View File
@@ -80,6 +80,19 @@ const T = (title, extra) => ({ title, notes: '', due: '', done: false, ...extra
r = await admin.call('/api/users', { name: ' ', tabs: [] }); ok('a name is required', r.status === 400, r);
r = await register(sam, dS, { inviteCode: samCode }, 'Sam phone', base); ok('Sam enrols with the code and is signed in as Sam', r.status === 200 && sam.cookie.startsWith('hub_session=' + samUser.id + '.'), [r.status, sam.cookie.slice(0, 24)]);
r = await register(client(base), device(), { inviteCode: samCode }, 'again', base); ok('the code works only once', r.status === 403, r);
// ---- recovery codes and the audit log
r = await sam.call('/api/auth/recovery'); ok('a person starts with no recovery codes', r.status === 200 && r.json.remaining === 0, r.json);
r = await sam.call('/api/auth/recovery', {}); const recCodes = r.json.codes; ok('making recovery codes gives 8 one-time codes', r.status === 200 && recCodes.length === 8 && recCodes.every(c => /^[A-Z2-9]{4}-[A-Z2-9]{4}$/.test(c)), r.json);
ok('and they are stored only as hashes', !fs.readFileSync(path.join(dir, 'recovery.json'), 'utf8').includes(recCodes[0]), 'plain code found on disk');
const lost = client(base);
r = await register(lost, device(), { inviteCode: recCodes[0] }, 'Sam new phone', base); ok('a recovery code enrols a new passkey and signs Sam in', r.status === 200 && lost.cookie.startsWith('hub_session=' + samUser.id + '.'), [r.status, lost.cookie.slice(0, 24)]);
r = await register(client(base), device(), { inviteCode: recCodes[0] }, 'again', base); ok('the same recovery code cannot be used twice', r.status === 403, r.status);
{ const pk = (await lost.call('/api/auth/passkeys')).json.find(p => p.name === 'Sam new phone'); await lost.call('/api/auth/passkeys', { id: pk.id }, 'DELETE'); } // tidy up so later checks see Sam's original passkeys
r = await sam.call('/api/auth/recovery'); ok('seven codes remain', r.json.remaining === 7, r.json);
r = await sam.call('/api/audit'); ok('an ordinary person cannot read the audit log', r.status === 403, r.status);
r = await admin.call('/api/audit'); const evs = (r.json.events || []).map(e => e.event);
ok('the admin sees the audit log: invite use, recovery codes and recovery use are recorded', ['recovery codes created', 'recovery code used', 'passkey added with invite'].every(e => evs.includes(e)), evs);
ok('and it never contains a code', !JSON.stringify(r.json).includes(recCodes[0]) && !JSON.stringify(r.json).includes(samCode), 'code leaked');
r = await sam.call('/api/me'); ok('Sam is a normal user with only Home, Settings and the tabs chosen', r.json.role === 'user' && r.json.tabs.sort().join() === 'calendar,home,search,settings,tasks', r.json);
// ---- private data