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
185 lines
8.1 KiB
Markdown
185 lines
8.1 KiB
Markdown
# stammtisch-hersbruck.de — Backend (PocketBase)
|
|
|
|
PocketBase-Instanz für Events, Läufe, Fahrer, Zeiten und Teams.
|
|
|
|
## Lokal starten
|
|
|
|
`.env` aus der Vorlage anlegen (die `.env` selbst steht in `.gitignore`):
|
|
|
|
cp .env.example .env
|
|
|
|
und die Werte eintragen — `.env.example` beschreibt jede Variable. Dann:
|
|
|
|
docker compose up -d --build
|
|
|
|
Admin-UI: http://127.0.0.1:8090/_/ — Login mit `SUPERUSER_EMAIL`/`SUPERUSER_PASSWORD`.
|
|
|
|
Damit das Frontend gegen diese Instanz läuft, in `../frontend/.env`
|
|
`PUBLIC_PB_URL=http://127.0.0.1:8090` setzen.
|
|
|
|
## Schema
|
|
|
|
Das Schema liegt als versionierte Migration in `pb_migrations/` und wird beim
|
|
Start automatisch angewendet. Es enthält die sechs fachlichen Collections
|
|
`users`, `teams`, `events`, `runs`, `riders`, `times`.
|
|
|
|
`1754400000_init_schema.js` ist ein Snapshot der produktiven Instanz. Er wurde
|
|
per `GET /api/collections` abgezogen und ruft `importCollections` mit
|
|
`deleteMissing = false` auf — die Migration legt an und aktualisiert, löscht
|
|
aber nichts, was nicht im Snapshot steht.
|
|
|
|
### Schema-Änderungen aus dem Admin-UI holen
|
|
|
|
`pb_migrations/` wird **nicht** in den Container gemountet — die Migrationen
|
|
kommen über `COPY` ins Image (siehe `Dockerfile`). Das ist Absicht: Ein
|
|
Bind-Mount überdeckt das Image-Verzeichnis, und auf einem Deployment-Host ohne
|
|
ausgechecktes Repo legt Docker dort ein leeres Verzeichnis an. PocketBase
|
|
findet dann keine Migration und startet mit einer Datenbank ohne Collections.
|
|
|
|
Die Kehrseite: Eine im Admin-UI erzeugte Migration liegt zunächst nur im
|
|
Container und muss von Hand herausgeholt werden:
|
|
|
|
# Welche Dateien sind dazugekommen?
|
|
docker compose exec pocketbase ls /pb/pb_migrations
|
|
|
|
# Die neue Datei ins Repo kopieren und committen
|
|
docker compose cp pocketbase:/pb/pb_migrations/<datei> ./pb_migrations/
|
|
|
|
Ohne diesen Schritt ist die Änderung beim nächsten `--build` verloren.
|
|
|
|
Ein frisch gestarteter Container hat damit dasselbe Schema wie die produktive
|
|
Instanz — aber **keine** Daten.
|
|
|
|
### Regeln: der Login-Vorspann ist Pflicht
|
|
|
|
Jede Regel, die `@request.auth.id` erwähnt, beginnt mit
|
|
`@request.auth.id != "" && (…)`. Das ist kein Stilmittel, sondern schließt ein
|
|
Loch:
|
|
|
|
**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 — und ohne Admins ist jedes frisch angelegte
|
|
Team. Dasselbe gilt für jede optionale Relation: `created_by`, `stewards`,
|
|
`rider.user`.
|
|
|
|
Nachgewiesen an einer lokalen Instanz mit dem Schema aus diesem Repo: Ein
|
|
`POST` auf `/api/collections/riders/records` **ohne jeden Token** legte den
|
|
Fahrer an. Betroffen waren nach demselben Muster `teams` (update), `riders`,
|
|
`trails`, `trail_versions`, `trail_flags`, `trail_markers`, `trail_comments`,
|
|
`event_series` und `event_participants`.
|
|
|
|
`1754501700_rules_require_login.js` setzt den Vorspann vor jede betroffene
|
|
Regel. Er kann nichts erlauben, was vorher verboten war — er nimmt nur die
|
|
anonyme Anfrage heraus, die keine dieser Regeln je erfüllen sollte. Gelesen
|
|
werden die Regeln aus der Datenbank, eine Handänderung aus dem Admin-UI wird
|
|
also abgesichert und nicht überschrieben.
|
|
|
|
Nach dem Deploy einmal gegenprüfen — das muss scheitern:
|
|
|
|
curl -X POST https://api.stammtisch-hersbruck.de/api/collections/riders/records \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"team":"<TEAM_ID>","name":"Test"}'
|
|
|
|
Wer eine neue Regel schreibt, setzt den Vorspann mit. Sonst reißt dasselbe
|
|
Loch an der nächsten Stelle wieder auf.
|
|
|
|
### `1754501300` scheitert auf einer frischen Datenbank
|
|
|
|
Bekannt und **nicht behoben**: Bei `docker compose up --build` mit leerem
|
|
`pb_data` legt der Snapshot `1754400000` die Collection `riders` bereits ohne
|
|
das Feld `event` an. `1754501300_event_participants.js` sucht danach mit
|
|
`findRecordsByFilter('riders', 'event != "")` und bricht mit
|
|
`unknown field "event"` ab — PocketBase startet dann gar nicht.
|
|
|
|
Auf der produktiven Instanz tritt das nicht auf, dort gibt es das Feld noch.
|
|
Angefasst wurde die Migration deshalb nicht; wer lokal von Null startet,
|
|
braucht diese eine Zeile:
|
|
|
|
const existing = riders.fields.getByName('event')
|
|
? app.findRecordsByFilter('riders', 'event != ""', '', 0, 0)
|
|
: []
|
|
|
|
## Hooks (`pb_hooks/`)
|
|
|
|
Serverseitiges JavaScript, das PocketBase beim Start lädt (`--hooksDir`). Wie
|
|
`pb_migrations/` kommt das Verzeichnis per `COPY` ins Image — eine Änderung
|
|
wirkt also erst nach `docker compose up -d --build`.
|
|
|
|
Bisher liegt dort ein Anliegen: die öffentlichen Routen der Einladungslinks.
|
|
|
|
| Datei | Zweck |
|
|
|---|---|
|
|
| `team_invites.pb.js` | `GET /api/team-invites/{token}`, `POST …/accept`, `POST …/join` |
|
|
| `team_invites_lib.js` | Gemeinsame Helfer der drei Routen |
|
|
|
|
Warum überhaupt ein Hook? Wer über einen Link beitritt, ist noch nicht
|
|
angemeldet und muss drei Dinge auf einmal tun, die keine Collection-Regel
|
|
einzeln erlauben darf: ein Konto anlegen, sich in `teams.users` eintragen und
|
|
einen Fahrer anlegen. Ein Hook arbeitet an den Regeln vorbei und kann genau
|
|
diese Kombination erlauben — gegen einen gültigen Token und sonst nicht.
|
|
|
|
**Zwei Fallstricke, an denen die erste Fassung scheiterte:**
|
|
|
|
1. Jeder Route-Handler läuft in einer **eigenen** JS-Laufzeit. Funktionen, die
|
|
in derselben Datei neben `routerAdd` stehen, sind im Handler *nicht*
|
|
sichtbar (`ReferenceError`). Gemeinsamer Code gehört deshalb in ein Modul,
|
|
das der Handler sich per ``require(`${__hooks}/…`)`` selbst holt.
|
|
2. Was `record.get()` bei einer Mehrfach-Relation liefert, ist ein Go-Slice
|
|
und kein JS-Array — `indexOf` und Konsorten sind darauf nicht verlässlich.
|
|
Anhängen geht über den Modifier `record.set('users+', id)`, Prüfen über
|
|
einen Filter.
|
|
|
|
## Datenpersistenz
|
|
|
|
Sämtliche Laufzeitdaten liegen in der SQLite-DB unter `/pb/pb_data`: Datensätze,
|
|
Uploads, die SMTP-Einstellungen und der Superuser. Ohne gemountetes Volume auf
|
|
diesem Pfad sind sie nach jedem Redeploy verloren.
|
|
|
|
Das Volume-Mapping `./pb_data:/pb/pb_data` steht in der `docker-compose.yaml`,
|
|
damit die Persistenz-Konfiguration versioniert im Repo liegt.
|
|
|
|
**Trügerisches Signal:** Die Collections kommen aus der Migration und sind nach
|
|
einem Redeploy auch dann da, wenn gar kein Volume existiert. Sie taugen **nicht**
|
|
als Nachweis funktionierender Persistenz — das zeigt nur ein Datensatz, der in
|
|
keiner Migration steht.
|
|
|
|
Mount prüfen:
|
|
|
|
docker inspect <container> --format '{{json .Mounts}}' | jq
|
|
|
|
Erscheint dort kein Eintrag mit `"Destination": "/pb/pb_data"`, fehlt die Persistenz.
|
|
|
|
`pb_data/` steht in `.gitignore` und gehört dort auch hin.
|
|
|
|
## Superuser-Verhalten
|
|
|
|
`entrypoint.sh` ruft `superuser create` auf — bewusst **nicht** `upsert`.
|
|
Existiert der Account bereits, scheitert `create` mit „Value must be unique" und
|
|
das Passwort bleibt unangetastet. Eine im Admin-UI vorgenommene
|
|
Passwortänderung überlebt damit jeden Redeploy.
|
|
|
|
| Situation | Verhalten |
|
|
|---|---|
|
|
| Erster Start, Variablen gesetzt | Superuser wird angelegt |
|
|
| Neustart, Account existiert | „Superuser existiert bereits — Passwort bleibt unverändert" |
|
|
| Passwort im UI geändert, dann Neustart | UI-Passwort gilt weiter, Env wird ignoriert |
|
|
| Variablen nicht gesetzt | Übersprungen, PocketBase gibt Installer-Link im Log aus |
|
|
| Passwort zu kurz (< 8 Zeichen) | Container bricht mit Exitcode 1 ab |
|
|
|
|
Passwort später ändern: **im Admin-UI**, nicht über die Env — eine Änderung der
|
|
Env-Variable hat keine Wirkung mehr, sobald der Account existiert.
|
|
|
|
## PocketBase aktualisieren
|
|
|
|
Die Version ist über das Build-Arg `PB_VERSION` gepinnt (Dockerfile und
|
|
`docker-compose.yaml`). Wert hochsetzen, committen, neu bauen.
|
|
|
|
Ein `latest` gibt es bewusst nicht: Der GitHub-Asset heißt
|
|
`pocketbase_<version>_linux_amd64.zip`, enthält die Version also im Dateinamen.
|
|
Der Pin ist auch gewollt — sonst zieht jeder Rebuild unangekündigt eine neue
|
|
Version, inklusive möglicher DB-Schema-Migrationen.
|
|
|
|
Vor einem Update: Changelog prüfen
|
|
(https://github.com/pocketbase/pocketbase/releases) und `pb_data` sichern —
|
|
PocketBase migriert die DB beim Start automatisch, ein Downgrade ist danach
|
|
nicht mehr ohne Weiteres möglich.
|