Multiple people: an admin adds users and chooses which tabs each may use
build-container / build (push) Successful in 2m20s
build-container / build (push) Successful in 2m20s
- Accounts: the setup code (or an existing single-user install, migrated automatically) creates the admin; the admin adds people in Settings -> People and gives each a one-time invite code to enrol their own passkey - Each person's tasks, NE Hub events and subscribe link, settings, calendar links, iCloud connection and Plex sign-in are private, stored in their own folder (/data/users/<id>/); Plex sign-in moved from browser cookies into that folder (encrypted), so it also follows them across devices - Tab access is enforced by the server (403) and reflected in the app (nav, Home, Settings, search, /api/config hides addresses of tabs a person cannot use); browser storage is namespaced per person - Admin tools: change tabs, invite a device, sign out everywhere (session epoch), turn off, make/remove admin, delete a person and their data; the last admin cannot be removed - Sessions carry the person (uid.epoch.expiry.signature); old single-user cookies still work as the admin - tools/users-test.js (83 checks: isolation, access limits, invites, sign-out/disable/delete, Plex and iCloud per person, migration) now runs in the Gitea workflow before the image is built Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
489ea0aaf0
commit
f18d9dd7a9
16 files changed
+703
-165
No files matched your search
+7
-4
@@ -17,19 +17,22 @@ const targets = [...new Set([...src.matchAll(/return (\w+)\(req, res\)/g)].map(m
|
||||
const missing = targets.filter(n => !new RegExp(`(async )?function ${n}\\b|const ${n} =`).test(src));
|
||||
ok('every function the router calls is defined', !missing.length, missing);
|
||||
|
||||
const child = spawn(process.execPath, ['server.js'], { cwd: root, env: { ...process.env, PORT: String(port), DATA_DIR: dir, COOKIE_SECRET: 'smoke-'.repeat(8), HUB_SETUP_CODE: 'smoke', SEAFILE_URL: 'http://127.0.0.1:9' } });
|
||||
// Seed one admin account so the test can sign in without a passkey ceremony (the cookie is signed with the test's own secret).
|
||||
const SECRET = 'smoke-'.repeat(8), UID = 'usmoke';
|
||||
fs.writeFileSync(path.join(dir, 'users.json'), JSON.stringify({ users: [{ id: UID, name: 'Admin', role: 'admin', tabs: [], handle: 'aGFuZGxl', epoch: 1, disabled: false, created: Date.now() }] }));
|
||||
const child = spawn(process.execPath, ['server.js'], { cwd: root, env: { ...process.env, PORT: String(port), DATA_DIR: dir, COOKIE_SECRET: SECRET, HUB_SETUP_CODE: 'smoke', SEAFILE_URL: 'http://127.0.0.1:9' } });
|
||||
let alive = true, log = ''; child.on('exit', () => { alive = false; }); child.stdout.on('data', d => log += d); child.stderr.on('data', d => log += d);
|
||||
const wait = ms => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
(async () => {
|
||||
for (let i = 0; i < 50; i++) { try { await fetch(base + '/login'); break; } catch { await wait(100); } }
|
||||
const exp = Date.now() + 864e5, sig = crypto.createHmac('sha256', 'smoke-'.repeat(8)).update('s' + exp).digest('base64url');
|
||||
const cookie = `hub_session=${exp}.${sig}`;
|
||||
const exp = Date.now() + 864e5, sig = crypto.createHmac('sha256', SECRET).update(`s${UID}.1.${exp}`).digest('base64url');
|
||||
const cookie = `hub_session=${UID}.1.${exp}.${sig}`;
|
||||
const get = async (p, method = 'GET', body) => { try { const r = await fetch(base + p, { method, redirect: 'manual', headers: { cookie, 'content-type': 'application/json' }, body: body && JSON.stringify(body) }); await r.text(); return r.status; } catch { return -1; } };
|
||||
|
||||
const pages = ['/', '/login', '/app.js', '/style.css', '/sw.js', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];
|
||||
const api = ['/api/config', '/api/system', '/api/settings', '/api/auth/state', '/api/auth/passkeys', '/api/tasks/lists', '/api/tasks?list=local:inbox', '/api/tasks/summary',
|
||||
'/api/cal/list', '/api/cal/icloud', '/api/cal/feed', '/api/search?q=ab', '/api/search?q=a', '/api/ddg?q=einstein', '/api/ddg?q=a', '/api/plex/status',
|
||||
'/api/me', '/api/users', '/api/cal/list', '/api/cal/icloud', '/api/cal/feed', '/api/search?q=ab', '/api/search?q=a', '/api/ddg?q=einstein', '/api/ddg?q=a', '/api/plex/status',
|
||||
`/api/cal/events?from=${Date.now()}&to=${Date.now() + 864e5}&fd=2026-01-01&td=2026-01-02`];
|
||||
const bad = [];
|
||||
for (const p of [...pages, ...api]) { const st = await get(p); if (st === 500 || st === -1 || !alive) bad.push(`${p} -> ${alive ? st : 'server died'}`); }
|
||||
|
||||
Reference in new issue
Block a user