feat(collections): per-member permission roles on shared lists (#1081)

The owner now assigns each member a role — viewer (read + copy-to-trip only),
editor (default: add + edit places) or admin (full incl. delete). The owner is
always full. Existing members default to editor via migration 152, so nothing
regresses.

- Server: role column on collection_members (migration 152 + schema); roleOf +
  assertCanEdit (save/update/status/list-meta) + assertCanDelete (delete) layered
  on assertAccess; sendInvite takes a role; new setMemberRole (owner-only) +
  POST members/role; members payload carries each role.
- Client: Share modal gains a role picker on invite and a per-member role select
  for the owner (read-only role badge for others); the page hides add / edit /
  status / move / delete for roles that can't perform them (server still enforces).
- Roles in all 22 locales; service + controller tests for the new gating.
This commit is contained in:
Maurice
2026-07-01 17:11:16 +02:00
committed by Maurice
parent f2972481fe
commit 1e0feb93dd
35 changed files with 263 additions and 40 deletions
+5
View File
@@ -3261,6 +3261,11 @@ function runMigrations(db: Database.Database): void {
try { db.exec('ALTER TABLE collections ADD COLUMN links TEXT'); } catch (err) { console.warn('[migrations] Non-fatal migration step failed:', err); }
try { db.exec('ALTER TABLE collection_places ADD COLUMN links TEXT'); } catch (err) { console.warn('[migrations] Non-fatal migration step failed:', err); }
},
// Migration 152: per-member permission role on a shared list. Existing
// accepted members default to 'editor' so nothing regresses.
() => {
try { db.exec("ALTER TABLE collection_members ADD COLUMN role TEXT NOT NULL DEFAULT 'editor'"); } catch (err) { console.warn('[migrations] Non-fatal migration step failed:', err); }
},
];
if (currentVersion < migrations.length) {
+1
View File
@@ -391,6 +391,7 @@ function createTables(db: Database.Database): void {
collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'pending',
role TEXT NOT NULL DEFAULT 'editor',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, user_id)
);