Multiple people: an admin adds users and chooses which tabs each may use
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:
nmemmertandClaude Sonnet 5.5 committed 2026-10-20 16:35:14 -04:00
1 parent 489ea0aaf0
commit f18d9dd7a9
16 files changed
+703 -165

No files matched your search

+4 -2
View File
@@ -2,6 +2,7 @@
// because those need the person's own Seafile / Plex sign-in.
const cal = require('./cal');
const tasks = require('./tasks');
const users = require('./users');
const json = (res, code, obj) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(obj)); };
const ymd = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
@@ -11,11 +12,12 @@ async function handle(req, res) {
if (q.length < 2) return json(res, 200, { tasks: [], events: [], errors: [] });
const errors = [];
let foundTasks = [], foundEvents = [];
try {
const me = users.current();
if (users.can(me, 'tasks')) try {
foundTasks = (await tasks.allTasks()).filter(t => (t.title + ' ' + t.notes).toLowerCase().includes(q))
.sort((a, b) => (a.done - b.done) || (a.due || '9999').localeCompare(b.due || '9999')).slice(0, 8);
} catch (e) { errors.push('Tasks: ' + e.message); }
try { // two months back to just over a year ahead
if (users.can(me, 'calendar')) try { // two months back to just over a year ahead
const now = new Date(), from = new Date(now); from.setDate(from.getDate() - 60); from.setHours(0, 0, 0, 0);
const to = new Date(now); to.setDate(to.getDate() + 400); to.setHours(23, 59, 59, 999);
const r = await cal.collectEvents({ from: from.getTime(), to: to.getTime(), fd: ymd(from), td: ymd(to) });