stammtisch-hersbruck/backend/pb_migrations/1754501700_rules_require_login.js
Daniel Michelberger 7ac5e9ab4f feat: Einladungslinks, mit denen sich Fahrer selbst ein Konto anlegen
Bisher legte die Teamleitung jedem Fahrer von Hand ein Konto an und gab
das Passwort weiter. Bei zwanzig Leuten in einer WhatsApp-Gruppe ist ein
Link die kuerzere Strecke.

Die oeffentliche Seite der Einladung laeuft ueber eigene Routen in
pb_hooks, nicht ueber die Collection: Ohne Login ist dort nichts
sichtbar, auch nicht mit Token. Der Token kommt vom Server, ein
mitgeschickter wird abgewiesen.

Dazu der Login-Vorspann vor jeder Regel, die @request.auth.id erwaehnt.
Das ist ein Loch und keine Kosmetik: Eine leere Relation ist in
PocketBase gleich dem leeren @request.auth.id einer anonymen Anfrage —
ein Team ohne Admins stuende sonst offen im Netz.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
2026-09-07 22:09:14 +02:00

96 lines
3.6 KiB
JavaScript

/// <reference path="../pb_data/types.d.ts" />
// Jede Regel, die auf `@request.auth.id` zeigt, bekommt einen Login-Vorspann.
//
// ACHTUNG, das ist ein Loch und keine Kosmetik: Eine *leere* Relation ist in
// PocketBase gleich dem *leeren* `@request.auth.id` einer anonymen Anfrage.
// `team.admins.id ?= @request.auth.id` ist damit für jedes Team ohne Admins
// wahr — auch ohne jeden Login. Dasselbe gilt für jede optionale Relation,
// die leer sein kann: `created_by`, `stewards`, `rider.user`.
//
// Nachgewiesen an einer frischen Instanz mit einem Team ohne Admins:
// Ein POST auf /api/collections/riders/records **ganz ohne Token** legte den
// Fahrer an. Betroffen sind nach demselben Muster
//
// teams update (admins leer)
// riders create/update/delete
// trails update/delete, trail_versions create/delete
// trail_flags create/update/delete
// trail_markers update/delete (created_by, stewards)
// trail_comments update/delete (created_by, stewards)
// event_series update/delete (created_by)
// event_participants create/update/delete (event.created_by)
//
// Der Vorspann `@request.auth.id != "" && (…)` kann nichts erlauben, was
// vorher verboten war — er nimmt nur die anonyme Anfrage heraus, die keine
// dieser Regeln je erfüllen sollte. Deshalb wird er stumpf vor jede Regel
// gesetzt, die `@request.auth.id` erwähnt, statt die betroffenen Stellen
// einzeln zu suchen: Was hier nicht auffällt, fällt sonst gar nicht auf.
//
// Gelesen werden die Regeln aus der Datenbank, nicht aus dieser Datei. Eine
// im Admin-UI von Hand geänderte Regel wird also mit ihrem eigenen Wortlaut
// abgesichert und nicht überschrieben.
const GUARD = '@request.auth.id != ""'
const COLLECTIONS = [
'users', 'teams', 'events', 'runs', 'riders', 'times',
'trails', 'trail_versions', 'trail_flags', 'trail_markers', 'trail_comments',
'event_participants', 'event_series', 'team_invites',
]
const RULES = ['listRule', 'viewRule', 'createRule', 'updateRule', 'deleteRule']
migrate((app) => {
for (const name of COLLECTIONS) {
let collection
try {
collection = app.findCollectionByNameOrId(name)
} catch (err) {
continue // Collection gibt es auf dieser Instanz (noch) nicht.
}
let changed = false
for (const key of RULES) {
const rule = collection[key]
// Leere Regel heißt „nur Superuser" und braucht keinen Schutz;
// eine Regel ohne @request.auth.id ist nicht gemeint.
if (!rule || rule.indexOf('@request.auth.id') === -1) continue
if (rule.indexOf(GUARD) === 0) continue
collection[key] = GUARD + ' && (' + rule + ')'
changed = true
}
if (changed) app.save(collection)
}
}, (app) => {
const prefix = GUARD + ' && ('
for (const name of COLLECTIONS) {
// team_invites bringt den Vorspann aus 1754501600 mit; ihn hier
// abzuräumen hieße, ein Loch zu graben, das es vorher nicht gab.
if (name === 'team_invites') continue
let collection
try {
collection = app.findCollectionByNameOrId(name)
} catch (err) {
continue
}
let changed = false
for (const key of RULES) {
const rule = collection[key]
if (!rule || rule.indexOf(prefix) !== 0 || rule.charAt(rule.length - 1) !== ')') continue
collection[key] = rule.slice(prefix.length, -1)
changed = true
}
if (changed) app.save(collection)
}
})