docs: Benutzernamen in Console-Klickliste, API-README und README

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 08:34:15 +00:00
1 parent dc6bd3362b
commit 4e9659ca85
2 files changed
+7 -7

No files matched your search

+4 -4
View File
@@ -36,11 +36,11 @@ Alle Antworten sind JSON; Fehler haben die Form `{"error": "<deutscher Text>", "
| Endpunkt | Auth | Antworten |
|---|---|---|
| `POST /api/veroeffentlichen` `{title, issuer?, tags?, displayName?, imageFileId, previewFileId, sourceCardId}` | Nutzer-JWT | `201 {id}` · `401` · `403` Konto ohne Benutzernamen („Bitte wähle zuerst einen Benutzernamen.“, vor allen anderen Prüfungen) · `400` Validierung, Datei fehlt („Bild nicht gefunden.“), falscher Typ oder > 5 MiB, falsche Bildmaße („Das Bild hat nicht das Kartenformat.“) · `403` Datei nicht meine / falscher Bucket („Diese Datei gehört dir nicht.“), Datei schon veröffentlicht oder gesperrt („Diese Bilder sind bereits veröffentlicht oder gesperrt.“), Karte nicht meine („Diese Karte gehört nicht zu deinem Konto.“) · `409` „Diese Karte ist schon veröffentlicht.“ · `429` „Du hast schon 20 Karten veröffentlicht. Zieh eine zurück, um eine neue zu veröffentlichen.“ |
| `POST /api/veroeffentlichen` `{title, issuer?, tags?, imageFileId, previewFileId, sourceCardId}` | Nutzer-JWT | `201 {id}` · `401` · `403` Konto ohne Benutzernamen („Bitte wähle zuerst einen Benutzernamen.”, vor allen anderen Prüfungen) · `400` Validierung, Datei fehlt („Bild nicht gefunden.”), falscher Typ oder > 5 MiB, falsche Bildmaße („Das Bild hat nicht das Kartenformat.”) · `403` Datei nicht meine / falscher Bucket („Diese Datei gehört dir nicht.”), Datei schon veröffentlicht oder gesperrt („Diese Bilder sind bereits veröffentlicht oder gesperrt.”), Karte nicht meine („Diese Karte gehört nicht zu deinem Konto.”) · `409` „Diese Karte ist schon veröffentlicht.” · `429` „Du hast schon 20 Karten veröffentlicht. Zieh eine zurück, um eine neue zu veröffentlichen.” |
| `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` |
| `POST /api/logos/hochladen` `{name, displayName?, mono?, fileId}` | Nutzer-JWT | `201 {id}` (Zeilen-ID des Logos, ein Hash aus Konto und Datei, nicht die Datei-ID) · `401` · `403` Konto ohne Benutzernamen (wie beim Veröffentlichen) · `400` Validierung, Datei fehlt („Bild nicht gefunden.“), kein PNG („Das Logo muss eine PNG-Datei sein.“), größer als 1 MiB („Das Logo ist größer als 1 MB.“), Maße außerhalb 8 × 8 bis 720 × 250 („Das Logo hat ungültige Maße (8 × 8 bis 720 × 250 Pixel).“) · `403` Datei nicht meine / falscher Bucket („Diese Datei gehört dir nicht.“), Datei gesperrt („Diese Bilder sind bereits veröffentlicht oder gesperrt.“) · `409` „Diese Datei wird schon für ein Logo verwendet.“ · `429` „Du hast schon 30 Logos hochgeladen. Lösche eins, um ein neues hochzuladen.“ |
| `POST /api/logos/hochladen` `{name, mono?, fileId}` | Nutzer-JWT | `201 {id}` (Zeilen-ID des Logos, ein Hash aus Konto und Datei, nicht die Datei-ID) · `401` · `403` Konto ohne Benutzernamen (wie beim Veröffentlichen) · `400` Validierung, Datei fehlt („Bild nicht gefunden.”), kein PNG („Das Logo muss eine PNG-Datei sein.”), größer als 1 MiB („Das Logo ist größer als 1 MB.”), Maße außerhalb 8 × 8 bis 720 × 250 („Das Logo hat ungültige Maße (8 × 8 bis 720 × 250 Pixel).”) · `403` Datei nicht meine / falscher Bucket („Diese Datei gehört dir nicht.”), Datei gesperrt („Diese Bilder sind bereits veröffentlicht oder gesperrt.”) · `409` „Diese Datei wird schon für ein Logo verwendet.” · `429` „Du hast schon 30 Logos hochgeladen. Lösche eins, um ein neues hochzuladen.” |
| `POST /api/logos/herz` `{id, herz}` | Nutzer-JWT | `200 {herz, likeCount}` · `401` · `400` ID ungültig oder `herz` kein Boolean · `404` fehlt oder ausgeblendet („Dieses Logo ist nicht (mehr) verfügbar.“) |
| `POST /api/logos/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`; dasselbe Limit wie `/api/melden`) |
| `POST /api/logos/loeschen` `{id}` | Nutzer-JWT (Hochlader oder Admin) | `200 {ok:true}` · `401` · `400` · `403` fremd · `404` fehlt |
@@ -68,11 +68,11 @@ Jede Fehlerantwort trägt neben dem deutschen Text einen `code`: den Schlüssel
### 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). Nicht erlaubt in Titel, Herausgeber, Anzeigename und Tags: Steuerzeichen (U+0000–001F, U+007F–009F), Nullbreiten- und Bidi-Zeichen (U+200B–200F, U+202A–202E, U+2060–206F) und das BOM (U+FEFF); sie machen Titel unsichtbar oder ordnen Text um. Dasselbe Muster prüft der Veröffentlichen-Dialog. `tags` ist ein `string[]`: getrimmt, klein, Leere und Doppelte entfernt, höchstens 5 mit je höchstens 20 Zeichen, kein Komma. Gespeichert wird die Spalte `tags` als `,tag1,tag2,` (Komma an beiden Enden, leer bei keinen Tags): der Tag-Filter der Website sucht `,<tag>,` und trifft so nur ganze Tags. Bild und Vorschau müssen verschiedene Dateien sein.
- **Felder**: Titel 1–60, Herausgeber ≤ 40 Zeichen (getrimmt). Nicht erlaubt in Titel, Herausgeber und Tags: Steuerzeichen (U+0000–001F, U+007F–009F), Nullbreiten- und Bidi-Zeichen (U+200B–200F, U+202A–202E, U+2060–206F) und das BOM (U+FEFF); sie machen Titel unsichtbar oder ordnen Text um. Dasselbe Muster prüft der Veröffentlichen-Dialog. `tags` ist ein `string[]`: getrimmt, klein, Leere und Doppelte entfernt, höchstens 5 mit je höchstens 20 Zeichen, kein Komma. Gespeichert wird die Spalte `tags` als `,tag1,tag2,` (Komma an beiden Enden, leer bei keinen Tags): der Tag-Filter der Website sucht `,<tag>,` und trifft so nur ganze Tags. Bild und Vorschau müssen verschiedene Dateien sein.
- **Quellkarte**: `sourceCardId` muss eine Zeile der Tabelle `cards` sein, deren `$permissions` `read("user:<ich>")` enthalten, sonst `403` „Diese Karte gehört nicht zu deinem Konto.“ (auch wenn es die Zeile nicht gibt).
- **Limit**: höchstens 20 Veröffentlichungen je Konto (Zeilen mit `ownerId` = ich, auch ausgeblendete); ab da `429` mit dem Text aus der Tabelle. Zurückziehen gibt einen Platz frei. Das Limit liest die Zeilen des Autors über `ownerId` (dort gibt es einen Index) mit `limit(20)`.
- **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` (Typ und Größe fremder Dateien nennt die API nicht). **Bildmaße**: Die API liest die ersten 64 Bytes beider Dateien (`GET …/files/<id>/view` mit Server-Key, der Rest der Übertragung wird abgebrochen) und liest PNG-IHDR bzw. WebP (`VP8 `, `VP8L`, `VP8X`) aus. Das Bild muss genau 1536 × 969 sein, die Vorschau 480 breit und 302–304 hoch (die Karte ist 1536 × 969, die Vorschau 480 × 303), sonst `400` „Das Bild hat nicht das Kartenformat.“ Das ist Defense in Depth gegen Dekompressionsbomben; die maßgebliche Prüfung beim Anzeigen macht der Client (Pixelgrenze 4 Megapixel). **Keine Wiederverwendung**: Beide Dateien müssen `read("any")` tragen (frisch hochgeladen; ausgeblendete Dateien nie) und dürfen von keiner Galerie-Zeile des Autors schon als Bild oder Vorschau verwendet werden (auch überkreuz, auch von ausgeblendeten Karten), sonst `403` „Diese Bilder sind bereits veröffentlicht oder gesperrt.“ So lassen sich die Dateien einer gemeldeten oder bereits veröffentlichten Karte nicht unter einer anderen `sourceCardId` neu einstellen. Die Prüfung läuft zusammen mit Limit, Quellkarte und Anlegen je Autor nacheinander. Nach dem Anlegen der Zeile setzt die API beide Dateien auf genau `read("any")` (**kein `delete("user:<ich>")` mehr**, kanonischer Zustand, auch wenn der Client mehr Rechte vergeben hat); scheitert das, wird die Zeile zurückgenommen und schon festgelegte Dateien bekommen `delete("user:<ich>")` zurück, damit der Client den Fehlversuch aufräumen kann (scheitert schon das Zurücknehmen, steht `rollback failed for card <id>` im Log). Ein 409 fasst die Dateirechte nie an. Bei Appwrite 1.8 liefert eine Datei aus einem anderen Bucket schon beim Abruf 404, das erscheint dann ebenfalls als `400` „Bild nicht gefunden.“
- **Logo-Felder** (`/api/logos/hochladen`): `name` 1–40 und `displayName` ≤ 30 Zeichen (getrimmt; leerer Anzeigename = „Anonym“; dieselben verbotenen Zeichen wie bei den Karten-Texten), `mono` fehlt = `false`, sonst muss es ein Boolean sein (`badMono`), `fileId` nach dem ID-Muster. **Zeilen-ID des Logos** = `sha256(<ownerId>|<fileId>)` (erste 32 Hex), **nicht** die Datei-ID; die Antwort `201 {id}` trägt diese ID. Dieselbe Datei desselben Kontos ergibt dieselbe ID (`409`). Die ID eines gelöschten Logos steht öffentlich in der Liste und in gespeicherten Karten; wäre sie die Datei-ID, könnte jeder eine Datei mit genau dieser ID anlegen, als Logo hochladen und so die alte ID neu belegen (gespeicherte Karten anderer zeigten dann sein Bild). Mit dem Konto im Hash geht das nicht. Dass eine Datei nie zwei Logos trägt, sichert der Besitz: nach dem Anlegen hat die Datei kein `delete("user:<ich>")` mehr, ein anderes Konto bekommt sie nicht (`403`). **Altlogos** (vor dieser Regel angelegt) haben noch Zeilen-ID = Datei-ID: Herz, Melden, Löschen und Sichtbarkeit adressieren die Zeile nur über ihre ID und die Datei über das Feld `fileId`, darum gehen beide Arten; dieselbe Datei eines Altlogos noch einmal hochzuladen ist ebenfalls `409`.
- **Logo-Felder** (`/api/logos/hochladen`): `name` 1–40 Zeichen (getrimmt; dieselben verbotenen Zeichen wie bei den Karten-Texten), `mono` fehlt = `false`, sonst muss es ein Boolean sein (`badMono`), `fileId` nach dem ID-Muster. **Zeilen-ID des Logos** = `sha256(<ownerId>|<fileId>)` (erste 32 Hex), **nicht** die Datei-ID; die Antwort `201 {id}` trägt diese ID. Dieselbe Datei desselben Kontos ergibt dieselbe ID (`409`). Die ID eines gelöschten Logos steht öffentlich in der Liste und in gespeicherten Karten; wäre sie die Datei-ID, könnte jeder eine Datei mit genau dieser ID anlegen, als Logo hochladen und so die alte ID neu belegen (gespeicherte Karten anderer zeigten dann sein Bild). Mit dem Konto im Hash geht das nicht. Dass eine Datei nie zwei Logos trägt, sichert der Besitz: nach dem Anlegen hat die Datei kein `delete("user:<ich>")` mehr, ein anderes Konto bekommt sie nicht (`403`). **Altlogos** (vor dieser Regel angelegt) haben noch Zeilen-ID = Datei-ID: Herz, Melden, Löschen und Sichtbarkeit adressieren die Zeile nur über ihre ID und die Datei über das Feld `fileId`, darum gehen beide Arten; dieselbe Datei eines Altlogos noch einmal hochzuladen ist ebenfalls `409`.
- **Logo-Datei und Reihenfolge**: Die Datei liegt im Bucket `logo-images`, trägt `delete("user:<ich>")` und `read("any")`, ist `image/png`, höchstens 1 MiB (1048576 Byte) und hat einen PNG-Kopf mit 8 × 8 bis 720 × 250 Pixeln (die API liest nur die ersten 64 Bytes). Die Prüfungen laufen in dieser Reihenfolge, damit fremde Dateien nichts verraten: JWT (`401`) → Benutzername (`403` `usernameRequired`) → Body (`400`) → Datei da und im richtigen Bucket (`400`/`403`) → je Konto nacheinander (Sperre `logo-owner:<id>`): schon 30 Logos (`429`, ausgeblendete zählen mit) → diese Datei ist schon ein Logo des Kontos (`409`: Zeile mit dieser Zeilen-ID oder mit dieser `fileId`, also auch ein Altlogo) → Besitz (`403`: kein `delete("user:<ich>")` oder kein `read("any")`) → Typ und Größe (`400`) → Maße (`400`) → ab hier zusätzlich die Sperre des Logos `logo:<Zeilen-ID>` (immer erst Konto, dann Logo) bis zur zweiten Prüfung unten: Zeile anlegen (`409`, wenn die ID schon vergeben ist) → Datei auf genau `read("any")` setzen (**kein `delete("user:<ich>")` mehr**, sonst ließe sich das Bild unter derselben ID tauschen). Scheitert das Festlegen, wird die Zeile zurückgenommen und nur dann bekommt die Datei `delete("user:<ich>")` zurück (scheitert schon das Zurücknehmen, steht `rollback failed for logo <id> (file <Datei-ID>)` im Log; scheitert danach die Rückgabe von `delete("user:<ich>")`, bleibt die Datei ohne Zeile und ohne dieses Recht liegen und `restore failed for logo <id> (file <Datei-ID>)` steht im Log, die Antwort bleibt in beiden Fällen der ursprüngliche Fehler). **Zweite Prüfung**: Danach liest die API den Dateianfang noch einmal und prüft PNG-Kopf und Maße erneut. Jetzt kann der Hochlader die Datei nicht mehr ändern; so lässt sich der Inhalt nicht zwischen der ersten Prüfung und dem Festlegen unter derselben ID austauschen (Löschen und Neuanlegen der Datei). Fällt die Datei jetzt durch oder fehlt sie, löscht die API die Datei mit dem Server-Key und die Zeile (beides wird versucht, auch wenn das erste scheitert) und antwortet `400` `logoFormat` bzw. `fileMissing`; Log: `logo <id> removed after upload check (<code>)`, bleibt etwas stehen `rollback failed for logo <id> (file <Datei-ID>)`. Ein Appwrite-Fehler beim zweiten Lesen wird wie ein Fehler beim Festlegen behandelt (`502`, Zeile zurückgenommen, die Datei bekommt `delete("user:<ich>")` zurück). Fremde, schon veröffentlichte oder gesperrte Dateien ergeben `403`, ohne dass die API ihren Inhalt liest. **Sperre des Logos beim Hochladen**: Die Zeile steht ab dem Anlegen öffentlich in der Liste; ein Herz, eine Meldung oder ein Löschen, das bis zum Ende der zweiten Prüfung eintrifft, wartet auf die Sperre und sieht danach den Endzustand (das fertige Logo oder `404`, wenn die Prüfung es entfernt hat). So bleibt keine Herz- oder Meldungszeile ohne Logo zurück.
- **Herzen** (`/api/logos/herz`): ein Herz je Konto und Logo, Zeilen-ID `sha256(<logoId>|<userId>)` (erste 32 Hex), Recht `read("user:<id>")`. `herz: true` legt die Zeile an (`409` = schon da, kein Fehler), `false` löscht sie (schon weg = kein Fehler). Danach ist `likeCount` immer `countRows` aller Herzen des Logos (neu gezählt, nie hoch- oder runtergezählt) und wird nur geschrieben, wenn er abweicht. Fehlendes oder ausgeblendetes Logo: `404`, auch beim Entfernen. Pro Logo läuft immer nur eine Änderung gleichzeitig (Sperre `logo:<id>`, auch gegen ein gerade laufendes Hochladen dieses Logos). Herzen werden nicht geloggt.
- **Logo melden** (`/api/logos/melden`): wie `/api/melden`, mit denselben Gründen, demselben Melder (Netz, gehasht) und **demselben Rate-Limit** (ein gemeinsamer Zähler je Anschluss für Karten und Logos: 6 Karten- und 5 Logo-Meldungen vom selben Anschluss ergeben bei der 11. ein `429`). Die Zeilen-ID der Meldung ist `sha256(logo|<logoId>|<reporterHash>)`, `reportCount` ist `countRows` der Meldungen des Logos, ab 3 wird ausgeblendet (Datei und Zeile auf `read("user:<owner>")` und `read("label:admin")`).