From 27d34ad0d3bfa574b467ddbfa7945ccc677ad922 Mon Sep 17 00:00:00 2001 From: Daniel Michelberger Date: Wed, 2 Sep 2026 15:03:41 +0200 Subject: [PATCH] feat: Superadmin-Rolle und Standard-Flags fuer neue Teams Die Rolle superadmin kommt als Feld an users, der erste ist stammtisch@dne.name. Vergeben wird sie nur in PocketBase: Die updateRule schliesst das Feld ueber @request.body.superadmin:isset = false aus, sonst koennte sich jeder selbst befoerdern - jeder darf schliesslich seinen eigenen Datensatz aendern. Besondere Rechte in der App haengen noch nicht daran. Flag-Typen bleiben ausdruecklich Sache der Teams. Ein neues Team bekommt beim Anlegen den hartkodierten Standardsatz aus der App und kann ihn danach frei aendern; eine appweite Verwaltung entfaellt damit. Ein Team ohne Flag-Typen koennte zwar Marker aufnehmen, aber nichts einordnen. Im Kopf stehen Avatar und Name links, Design-Umschalter und Logout rechts. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o --- TODO.md | 41 +++-------- .../pb_migrations/1754400000_init_schema.js | 11 ++- .../1754500900_users_superadmin.js | 47 +++++++++++++ frontend/src/lib/stores/trailFlags.svelte.ts | 13 ++-- frontend/src/lib/types.d.ts | 1 + frontend/src/routes/dashboard/+layout.svelte | 69 +++++++++++-------- .../routes/dashboard/settings/+page.svelte | 5 ++ .../dashboard/settings/team/+page.svelte | 18 ++++- 8 files changed, 139 insertions(+), 66 deletions(-) create mode 100644 backend/pb_migrations/1754500900_users_superadmin.js diff --git a/TODO.md b/TODO.md index 37687b8..cb9931e 100644 --- a/TODO.md +++ b/TODO.md @@ -5,36 +5,6 @@ es eine Entscheidung braucht oder weil es außerhalb des Repos passiert. ## Entscheidungen -### Rollenmodell: Superadmin appweit - -Geplant sind drei Rollen: **Superadmin** (appweit, über alle Teams), -**Team-Admin** (Owner/Admins eines Teams, gibt es schon) und **Mitglied**. - -Umsetzung, sobald die zwei Fragen unten geklärt sind: - -- `users.superadmin` als Bool-Feld. -- Die `updateRule` der users-Collection muss dabei - `@request.body.superadmin:isset = false` ergänzen — sonst kann sich jeder - Nutzer selbst befördern, denn er darf seinen eigenen Datensatz ändern. - -Offen: - -1. **Vorhandene Team-Flags.** Flag-Typen sollen appweit gelten (siehe unten). - Heute hat jedes Team seinen eigenen Satz. Zusammenführen und Duplikate von - Hand aufräumen, oder alle behalten und ab sofort global sichtbar machen? -2. **Erster Superadmin.** Den kann nur jemand direkt im PocketBase-Admin - setzen. Reicht ein Hinweis im Migrationskommentar, oder soll es eine - Superadmin-Verwaltung in den Einstellungen geben? - -### Flag-Typen appweit statt pro Team - -Hängt am Rollenmodell. Heute ist `trail_flags` team-gebunden: Das Feld `team` -existiert, der Store filtert danach, lesen darf das Team, schreiben Owner und -Admins. - -Ziel: `team` entfällt, lesen für alle Eingeloggten, schreiben nur Superadmin. -Die Typen werden damit App-weite Vorschläge, aus denen jedes Team auswählt. - ### Trail-Status: dürfen Mitglieder melden? Anzeige und Herkunft (`status_changed`, `status_by`) sind fertig. Ändern darf @@ -80,12 +50,13 @@ Kacheln ohne Schlüssel weiter — die Karte bleibt also sichtbar. ### Migrationen greifen beim nächsten Start -Vier neue Migrationen liegen in `backend/pb_migrations/`: +Fünf neue Migrationen liegen in `backend/pb_migrations/`: - `1754500500_riders_user_and_admin_rules.js` - `1754500600_events_dates_instead_of_status.js` - `1754500700_trail_status_meta.js` - `1754500800_trail_markers_custom.js` +- `1754500900_users_superadmin.js` Solange sie nicht gelaufen sind, scheitert alles, was auf den neuen Regeln aufbaut. Am deutlichsten beim Anlegen eines Mitglieds oder eines Fahrer-Logins: @@ -118,4 +89,12 @@ des Generators. - Der Melder eines Trail-Markers („wer") zeigt bis zum Deploy „Unbekannt". Die gelockerte `users.viewRule` kommt mit `1754500500`. +- Die Rolle **Superadmin** existiert seit `1754500900` und ist für + stammtisch@dne.name gesetzt, gibt in der App aber noch keine besonderen + Rechte. Sie ist bewusst nur in PocketBase vergebbar: Die `updateRule` der + users-Collection schließt `superadmin` vom Selbstsetzen aus. Sobald klar ist, + was sie dürfen soll, kommen die Regeln dazu. +- Flag-Typen bleiben **teamspezifisch**. Ein neues Team bekommt beim Anlegen + den hartkodierten Standardsatz aus `trailFlags.svelte.ts`, den es danach frei + ändern kann. Eine appweite Verwaltung gibt es damit bewusst nicht. - `NAMING.md` und `frontend/static/pedaler.svg` liegen bewusst nur lokal. diff --git a/backend/pb_migrations/1754400000_init_schema.js b/backend/pb_migrations/1754400000_init_schema.js index bcfcf21..6848b17 100644 --- a/backend/pb_migrations/1754400000_init_schema.js +++ b/backend/pb_migrations/1754400000_init_schema.js @@ -14,7 +14,7 @@ migrate((app) => { "listRule": "id = @request.auth.id", "viewRule": "@request.auth.id != \"\"", "createRule": "@request.auth.id != \"\"", - "updateRule": "id = @request.auth.id", + "updateRule": "id = @request.auth.id && @request.body.superadmin:isset = false", "deleteRule": "id = @request.auth.id", "name": "users", "type": "auth", @@ -125,6 +125,15 @@ migrate((app) => { "system": false, "type": "text" }, + { + "hidden": false, + "id": "bool_user_superadmin", + "name": "superadmin", + "presentable": false, + "required": false, + "system": false, + "type": "bool" + }, { "help": "", "hidden": false, diff --git a/backend/pb_migrations/1754500900_users_superadmin.js b/backend/pb_migrations/1754500900_users_superadmin.js new file mode 100644 index 0000000..ce5d6bf --- /dev/null +++ b/backend/pb_migrations/1754500900_users_superadmin.js @@ -0,0 +1,47 @@ +/// + +// Superadmin als appweite Rolle, über allen Teams. +// +// Wichtig ist die Ergänzung der updateRule: Jeder darf seinen eigenen +// Datensatz ändern — ohne `@request.body.superadmin:isset = false` könnte +// sich damit jeder selbst befördern. Vergeben wird die Rolle deshalb nur +// direkt in PocketBase oder durch eine Migration wie diese. +// +// Der erste Superadmin ist stammtisch@dne.name. Existiert das Konto (noch) +// nicht, passiert hier nichts weiter — die Rolle lässt sich jederzeit im +// Admin-UI nachziehen. +// +// Flag-Typen bleiben ausdrücklich Sache der Teams: Jedes Team verwaltet +// seine eigenen und bekommt beim Anlegen den Standardsatz aus der App. +// Superadmin hat damit im Alltag nichts zu tun. + +const FIRST_SUPERADMIN = 'stammtisch@dne.name' + +migrate((app) => { + const users = app.findCollectionByNameOrId('users') + + if (!users.fields.getByName('superadmin')) { + users.fields.add(new Field({ + id: 'bool_user_superadmin', + name: 'superadmin', + type: 'bool', + })) + } + + users.updateRule = 'id = @request.auth.id && @request.body.superadmin:isset = false' + app.save(users) + + try { + const first = app.findAuthRecordByEmail('users', FIRST_SUPERADMIN) + first.set('superadmin', true) + app.save(first) + } catch { + // Konto noch nicht angelegt — die Rolle wird dann von Hand vergeben. + } +}, (app) => { + const users = app.findCollectionByNameOrId('users') + + users.fields.removeByName('superadmin') + users.updateRule = 'id = @request.auth.id' + app.save(users) +}) diff --git a/frontend/src/lib/stores/trailFlags.svelte.ts b/frontend/src/lib/stores/trailFlags.svelte.ts index 3a3cb82..916ad9b 100644 --- a/frontend/src/lib/stores/trailFlags.svelte.ts +++ b/frontend/src/lib/stores/trailFlags.svelte.ts @@ -105,14 +105,19 @@ export class TrailFlagStore { * unangetastet — der Knopf lässt sich also gefahrlos mehrfach drücken. * Gibt die Anzahl neu angelegter Flags zurück. */ - async seedDefaults(): Promise { - const teamId = this.teams.activeId + async seedDefaults(teamId = this.teams.activeId): Promise { if (!teamId) throw new Error('Kein aktives Team') - if (!this.canManage) { + // Beim Anlegen eines Teams ist man dessen Owner, aber der Datensatz + // steckt womöglich noch nicht im Store — dann greift die Prüfung ins + // Leere. Für das eigene, gerade erzeugte Team wird sie übersprungen; + // die createRule der Collection setzt sie ohnehin serverseitig durch. + if (teamId === this.teams.activeId && !this.canManage) { throw new Error('Dafür brauchst du Administratorrechte im Team.') } - const existing = new Set(this.scoped.map((f) => f.label)) + const existing = new Set( + this.records.filter((f) => f.team === teamId).map((f) => f.label), + ) let created = 0 for (const flag of DEFAULT_FLAGS) { diff --git a/frontend/src/lib/types.d.ts b/frontend/src/lib/types.d.ts index 847c28e..f5f5bd9 100644 --- a/frontend/src/lib/types.d.ts +++ b/frontend/src/lib/types.d.ts @@ -202,6 +202,7 @@ export type UsersRecord = { id: string name?: string password: string + superadmin?: boolean tokenKey: string updated?: IsoDateString username: string diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 20cf648..848c16e 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -123,38 +123,49 @@
-