Files
AirCardIOS/deploy/api/README.md
T
ClaudeandClaude Sonnet 5.5 dd1597862a feat(api): aircard-api für Veröffentlichen, Melden und Admin
Neuer abhängigkeitsfreier Node-22-Dienst in deploy/api/ (appwrite.js, app.js,
server.js), der alle Galerie-Schreibzugriffe mit dem Appwrite-Server-Key
ausführt: POST /api/veroeffentlichen, /zurueckziehen, /melden (Rate-Limit
10/h je Melder, IPv6 auf /64, Auto-Ausblenden ab 3), /admin/sichtbarkeit und
GET /api/gesundheit.

- JWT-Prüfung über GET /account (Header X-Aircard-Jwt), Admin = Label admin
- IDs per Muster geprüft, Pfadsegmente kodiert, "." und ".." abgelehnt
- deterministische Zeilen-IDs (Galerie, Meldung): Appwrite-409 ersetzt jede
  Doppelt-Prüfung; Sperre je Karte gegen verlorene Zählerstände
- Dateiprüfung: Bucket, delete("user:<ich>"), PNG/WebP, höchstens 5 MiB
- Appwrite-Aufrufe mit 10-s-Zeitlimit, Logs ohne Key, Salz, JWT und IPs
- Tests gegen einen Fake-Appwrite im Speicher (70 Tests), Compose-Service
  aircard-api (Router aircard-gallery-api) und README ergänzt

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 14:09:30 +00:00

6.3 KiB
Raw Blame History

aircard-api

Kleiner HTTP-Dienst (Node 22, keine Abhängigkeiten) für die Community-Galerie von aircard-ios.org. Er führt alle Schreibzugriffe mit dem Appwrite-Server-Key aus: Veröffentlichen, Zurückziehen, Melden (Auto-Ausblenden ab 3 verschiedenen Meldern) und die Admin-Sichtbarkeit. Die Website liest die Galerie direkt aus Appwrite; nur das Schreiben läuft hierüber.

Spec: docs/superpowers/specs/2026-10-04-aircard-community-design.md §7, §8, §11. Deployment: ../README.md.

Dateien

Datei Aufgabe
server.js Start: liest die Umgebung, http.createServer, Zeitlimits, sauberes Beenden
app.js Routen, Validierung, Rechte-Strings, Rate-Limit, Logik (createApp)
appwrite.js REST-Client mit Server-Key (createAppwrite)
*.test.js, fake-appwrite.js Tests und Fake-Appwrite im Speicher (gehören nicht auf den Server)

Auf den Server (/home/webklar/apps/aircard-ios.org/api/) kommen nur server.js, app.js und appwrite.js.

Umgebung

Variable Standard Bedeutung
AIRCARD_API_KEY – (Pflicht) Appwrite-Server-Key aircard-api (Scopes rows.read, rows.write, files.read, files.write)
AIRCARD_API_SALT – (Pflicht) Salz für die Melder-Hashes, 32 Byte hex
AIRCARD_APPWRITE_PROJECT_ID – (Pflicht) Projekt-ID (6ac2130a00289343d6d5)
AIRCARD_APPWRITE_ENDPOINT http://webklar-appwrite/v1 interne Appwrite-Adresse im Docker-Netz
AIRCARD_API_PORT 8088 Port des Dienstes

Fehlt eine Pflichtvariable, nennt die Meldung nur ihren Namen und der Prozess endet mit Exit-Code 1. Wer das Salz ändert, macht alle bisherigen Melder-Hashes ungültig: dieselben Personen können dieselbe Karte dann nochmal melden (die Karten selbst bleiben unberührt).

Endpunkte

Alle Antworten sind JSON; Fehler haben die Form {"error": "<deutscher Text>"}. Das Nutzer-JWT kommt im Header X-Aircard-Jwt und wird bei Appwrite (GET /account) geprüft; Admin = Label admin. Alles andere (auch falsche Methoden) ist 404; POST nur mit Content-Type: application/json (sonst 415), Body höchstens 8 KB (sonst 413).

Endpunkt Auth Antworten
POST /api/veroeffentlichen {title, issuer?, tags?, displayName?, imageFileId, previewFileId, sourceCardId} Nutzer-JWT 201 {id} · 401 · 400 Validierung, Datei fehlt („Bild nicht gefunden.“), falsches Format oder > 5 MiB · 403 Datei nicht meine / falscher Bucket · 409 „Diese Karte ist schon veröffentlicht.“
POST /api/zurueckziehen {id} Nutzer-JWT (Autor oder Admin) 200 {ok:true} · 401 · 400 · 403 fremd · 404 fehlt
POST /api/melden {id, reason} keine 200 {ok:true} oder 200 {schonGemeldet:true} · 400 Grund oder ID · 404 fehlt oder ausgeblendet · 429 „Zu viele Meldungen – bitte später nochmal.“ (mit Retry-After)
POST /api/admin/sichtbarkeit {id, sichtbar} Admin-JWT 200 {ok:true} · 401 · 403 kein Admin · 400 · 404
GET /api/gesundheit keine 200 {ok:true}

Ist Appwrite nicht erreichbar oder antwortet mit einem Fehler, antwortet der Dienst 502 „Der Dienst ist gerade nicht erreichbar.“ (Details nur im Log, ohne Secrets), bei unerwarteten Fehlern 500.

Regeln im Einzelnen

  • IDs: Jede ID im Body (id, imageFileId, previewFileId, sourceCardId) muss ^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$ erfüllen (nur Zeichenketten), sonst 400. appwrite.js kodiert zusätzlich jedes Pfadsegment und lehnt . und .. ab.
  • Felder: Titel 1–60, Herausgeber ≤ 40, Anzeigename ≤ 30 Zeichen (getrimmt, keine Steuerzeichen). tags ist ein string[]: getrimmt, klein, Leere und Doppelte entfernt, höchstens 5 mit je höchstens 20 Zeichen, kein Komma. Bild und Vorschau müssen verschiedene Dateien sein.
  • Dateien (beide): liegen im Bucket gallery-images, tragen delete("user:<ich>"), sind image/png oder image/webp und höchstens 5 MiB. Eine Datei, die es nicht gibt, ergibt 400; eine fremde 403. Bei Appwrite 1.8 liefert eine Datei aus einem anderen Bucket schon beim Abruf 404, das erscheint dann ebenfalls als 400 „Bild nicht gefunden.“
  • Doppelt veröffentlichen: Die Zeilen-ID der Galerie ist sha256(ownerId|sourceCardId) (erste 32 Hex). Eine zweite Veröffentlichung derselben Karte scheitert deshalb atomar an Appwrite (409), auch bei gleichzeitigen Anfragen. Nach dem Zurückziehen ist die Karte wieder frei.
  • Melden: Rate-Limit 10 pro Stunde und Melder (Speicher, geht bei Neustart verloren; abgelaufene Einträge werden entfernt, höchstens 50 000 Einträge). Der Melder-Hash ist HMAC-SHA256(salt, ip); IPv6-Adressen werden vorher auf ihr /64-Netz gekürzt, IPv4-gemappte IPv6 auf IPv4. Die Zeilen-ID der Meldung ist sha256(galleryId|reporterHash), eine zweite Meldung desselben Melders ergibt {schonGemeldet:true}. reportCount = Anzahl der Meldungszeilen der Karte; ab 3 wird ausgeblendet. Auch eine doppelte Meldung zählt neu und heilt so einen früher steckengebliebenen Zählerstand. Pro Karte läuft immer nur eine Änderung gleichzeitig.
  • Client-IP: X-Real-Ip (setzt Traefik), sonst erster Eintrag von X-Forwarded-For, sonst die Socket-Adresse. Der Dienst darf deshalb nur über Traefik erreichbar sein (kein ports: in Compose).
  • Rechte (genau diese Strings): sichtbar Zeile read("any"); ausgeblendet Zeile read("user:<owner>"), read("label:admin"); Datei sichtbar read("any"), delete("user:<owner>"); Datei ausgeblendet read("user:<owner>"), read("label:admin"), delete("user:<owner>"). Meldungszeilen haben keine eigenen Rechte (Tabelle: nur read("label:admin")).
  • Reihenfolge bei Mehrschritt-Aktionen (jede Aktion ist wiederholbar): Ausblenden/Einblenden ändert zuerst die Dateirechte, beim Einblenden danach die Meldungen löschen, die Zeile zuletzt. Zurückziehen löscht Dateien, Meldungen, dann die Zeile. Bleibt etwas hängen (502), heilt der nächste Versuch.
  • Logs: Zeilen beginnen mit [aircard-api]. Sie nennen Methode, Route, Status, Karten-IDs und Fehlertypen, nie Key, Salz, JWT, IPs oder Fehlertexte von Appwrite.

Tests

node --test "deploy/api/*.test.js"

Die Tests starten den Dienst auf Port 0 gegen einen Fake-Appwrite im Speicher (fake-appwrite.js, mit den Spaltenlängen der echten Tabellen) und greifen nie auf das echte Appwrite zu.