Die Zeilen-ID eines gelöschten Logos steht öffentlich in der Liste und in gespeicherten Karten. War sie die Datei-ID, konnte 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.
- logos.js: Zeilen-ID = hash32("<ownerId>|<fileId>") (logoRowId); die Antwort 201 {id} trägt sie. Dieselbe Datei desselben Kontos ergibt dieselbe ID
(409 logoTaken, bei einem Altlogo über die fileId der Zeile); alle anderen Regeln bleiben. Die Logzeilen nennen die Zeilen-ID, die Fehlerzeilen
zusätzlich die Datei-ID (sie folgt nicht mehr aus der Zeilen-ID, und die Zeile ist beim Aufräumen vielleicht schon weg).
- Altlogos (Zeilen-ID = Datei-ID, eines ist live) gehen weiter: Herz, Melden, Löschen und Sichtbarkeit adressieren nur die Zeile und lesen die Datei aus fileId.
- Tests: seedLogo legt Zeile und Datei mit verschiedenen IDs an; neu: logoRowId, die ID eines gelöschten Logos lässt sich nicht neu belegen, anderes Konto
bekommt eine verwendete Datei nicht (403), Altlogo (Herz, Meldung, Ausblenden/Einblenden, Löschen, erneutes Hochladen 409).
- README und Spec: ID-Regel. Der Client (upload-logo.ts) folgt im nächsten Commit; beide gehen zusammen live.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Der Hochlader behält delete auf seiner Datei, bis die API sie auf read("any") festlegt. Zwischen der ersten Prüfung
(Typ, Größe, PNG-Kopf, Maße) und dem Festlegen ließ sich die Datei unter derselben ID löschen und mit anderem Inhalt
neu anlegen (z. B. 4000 × 4000). Jetzt liest die API den Dateianfang nach dem Festlegen noch einmal, wenn der
Hochlader die Datei nicht mehr ändern kann:
- PNG-Kopf und Maße 8 × 8 bis 720 × 250 werden erneut geprüft. Fällt die Datei durch oder fehlt sie, löscht die API die
Datei (Server-Key) und die Zeile (beides wird versucht) und antwortet 400 logoFormat bzw. fileMissing. Log:
"logo <id> removed after upload check (<code>)", bleibt etwas stehen: "rollback failed for logo <id>".
- Ein Appwrite-Fehler beim zweiten Lesen wird wie ein Fehler beim Festlegen behandelt (502, Zeile zurückgenommen, die
Datei bekommt delete(owner) zurück).
- fake-appwrite.js: Testhilfe swapContentAfterPermissions (der Inhalt ändert sich nach dem nächsten Rechte-Update).
- Tests: 7 neue in logos.test.js (Austausch auf 4000 × 4000, weitere ungültige Inhalte, Datei weg, Appwrite-Fehler,
Aufräumen scheitert, Ablauffolge, weiterhin gültiger Inhalt), der Logtest prüft das neue Ereignis mit. Der
Karten-Ablauf ist unverändert (app.js und common.js nicht angefasst).
- Doku: api/README.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- common.js (aus app.js herausgelöst, Verhalten gleich): MSG, HttpError, Prüfungen, Rechte-Strings, Melder-Hash,
Bildmaße und die Dateiprüfungen checkFileExists/checkFileOwnership. Neu: die Logo-Texte in MSG, codeFor,
likeRowPermissions. app.js exportiert weiter alles wie bisher.
- Fehlerantworten sind jetzt {error, code}: code = MSG-Schlüssel des Textes, dynamische Prüftexte badRequest,
Appwrite-Fehler unavailable (502) bzw. internal (500).
- logos.js: POST /api/logos/hochladen, /herz, /melden, /loeschen und /api/admin/logo-sichtbarkeit nach dem Vorbild der
Karten-Endpunkte (Datei prüfen, Zeile anlegen, Datei auf read("any") festlegen, sonst Rücknahme; likeCount und
reportCount immer aus countRows; ein gemeinsames Rate-Limit für Karten- und Logo-Meldungen).
- appwrite.js: countRows. fake-appwrite.js: Tabellen logos, logo_likes, logo_reports mit Spaltenlängen, Bucket
logo-images und total in den Listen-Antworten.
- Tests: logos.test.js (59), code-Fälle und MSG-Eindeutigkeit in app.test.js (+7), countRows in appwrite.test.js (+2);
alle 111 bisherigen Tests unverändert grün (179 gesamt).
- Doku: api/README.md (Endpunkte, Fehlercodes, Logo-Regeln), deploy/README.md (Betrieb kopiert fünf Dateien).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Beim Veröffentlichen liest die API die ersten 64 Bytes beider Dateien (PNG-IHDR, WebP VP8/VP8L/VP8X) und verlangt Bild 1536x969,
Vorschau 480x302-304, sonst 400 "Das Bild hat nicht das Kartenformat."
- Veröffentlichte und ausgeblendete Dateien tragen kein delete(owner) mehr (Abweichung von Spec §7): kein Löschen und Neuanlegen
derselben Datei-ID nach dem Veröffentlichen; scheitert das Festlegen, bekommen die Dateien delete(owner) für den Client-Aufräumer zurück
- Melder-Identität aus dem IPv4-/24 bzw. IPv6-/48; das Rate-Limit zählt weiter je Anschluss
- Höchstens 20 Veröffentlichungen je Konto (429); sourceCardId muss eine eigene Zeile in `cards` sein (403)
- Steuer-, Nullbreiten- und Bidi-Zeichen in Titel, Herausgeber, Anzeigename und Tags abgelehnt
- Tags als ",a,b," gespeichert (exakter Tag-Filter)
- fehlgeschlagenes Zurücknehmen wird mit `rollback failed for card <id>` protokolliert
- README: Endpunkt-Tabelle, Regeln, Scopes (documents.*), akzeptiertes Risiko X-Real-Ip im Docker-Netz
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Veröffentlichen lehnt mit 403 "Diese Bilder sind bereits veröffentlicht oder
gesperrt." ab, wenn eine Datei kein read("any") trägt (ausgeblendet) oder von
einer Galerie-Zeile des Autors schon als Bild oder Vorschau verwendet wird
(auch überkreuz, auch ausgeblendete Karten). Die Nutzungsprüfung liest die
Zeilen des Autors über den vorhandenen ownerId-Index seitenweise mit Cursor
(auf die Datei-ID-Spalten gibt es keinen Index) und läuft zusammen mit dem
Anlegen je Autor nacheinander. 92 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Kein Zugriffsprotokoll je Anfrage mehr (abgewiesene Anfragen an das
unauthentifizierte /api/melden loggten sonst unbegrenzt); nur noch
Ereignis- und 5xx-Zeilen, höchstens 120 pro Minute mit Sammelzeile
- Compose: logging json-file, max-size 5m, max-file 2
- appwrite.js: 404 nur mit Typ row_not_found/storage_file_not_found als
"fehlt", listRows ohne rows-Array wirft, redirect: error
- Appwrite-Fehler: 502 bei 0/429/5xx, sonst 500 "Interner Fehler."
- Veröffentlichen legt beide Dateien auf die sichtbaren Rechte fest
(erst nach dem Anlegen der Zeile, Rollback bei Fehler)
- setFilePermissions mit Promise.allSettled, gemeinsamer safeToken-Helfer
- Tests: 85 (Log-Flut, Log-Budget, 404-Typen, Auth-Header, Konto-Antworten)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>