c0a18ac735cb691127ed4260685438dc307c2925
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c0a18ac735 |
Wettlauf beim Reservieren, 500er bei unsinnigen Eingaben
Drei P3-Funde aus der Durchsicht vom 14.09.: Reservieren: Statusprüfung und Schreiben waren getrennt - zwei gleichzeitige Anfragen konnten beide passieren, die zweite überschrieb Name und Token der ersten, ohne dass die es erfuhr. Jetzt entscheidet ein UPDATE mit Status-Bedingung; der Verlierer bekommt None und die Route meldet "schon weg" (Seite) bzw. 409 (API). /?limit=abc lieferte jedem anonymen Besucher einen internen Serverfehler, limit=-1 hiess in SQLite "alles". _limit_lesen() fällt bei Unsinn auf die Seitengrösse zurück und deckelt bei 500. Erfassen/Bearbeiten: int(category_id) und die Pydantic-Prüfung warfen im Handler - 500 statt Fehlermeldung. Jetzt Meldung; ausserdem werden die Angaben VOR den Bildern geprüft, damit bei abgelehnten Angaben keine verwaisten Bilddateien liegen bleiben. Vier neue Tests, jeder einmal gegen den alten Code gelaufen und dabei rot geworden. 84 lokal grün (HEIC-Test braucht pillow-heif, Docker). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e0212cfa2c |
Tests von 55 auf 12 Sekunden, Abhängigkeiten nur noch einmal installieren
Docker-Image bauen / build (push) Successful in 1m7s
Der Testlauf war zu langsam. Gemessen statt geraten: 0,68 s Vorbereitung pro Test, bei 81 Tests praktisch die ganze Laufzeit. Ursache war bcrypt. Pro Test wird ein Hash gebildet und geprüft, und das ist absichtlich langsam - genau das soll es im Betrieb sein. Der Aufwand ist jetzt über BCRYPT_ROUNDS einstellbar (Standard bleibt 12) und in den Tests auf 4 gesetzt: dort geht es um die Ablauflogik, nicht um die Stärke des Hashes. Der Dekompressionsbomben-Test erzeugte ein Bild mit 400 Megapixeln, allein dafür 4,8 s. Jetzt wird stattdessen die Grenze heruntergesetzt und ein kleines Bild verwendet - dieselbe Codestelle, ohne die Wartezeit. Ausserdem installierten Test- und Anwendungs-Image dieselben Abhängigkeiten zweimal. Dockerfile.test setzt nun auf dem gebauten Anwendungs-Image auf, der Testschritt kommt entsprechend danach. Dabei zwei Fallen, die beide auffielen, weil die Tests plötzlich wieder langsam waren: - Das Basis-Image bringt seinen eigenen Stand von app/ mit. Ohne erneutes Kopieren prüfen die Tests den Code des Basis-Images - ist es veraltet, läuft alles gegen alten Code und meldet Erfolg. - Der ENTRYPOINT des Anwendungs-Images startet eine Datenbank-Migration. Für Tests weder nötig noch erwünscht, darum geleert. Ganze Kette lokal durchgespielt: bauen, testen, Image prüfen - 22 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o |
||
|
|
adedaa9a0d |
Hell/Dunkel-Umschaltung und Versionsangabe in der Fusszeile
Farbschema folgt zunächst der Systemeinstellung, lässt sich per Knopf in der Kopfzeile umstellen und wird dann gemerkt. Bei gesperrtem Speicher (privates Fenster) greift wieder die Systemeinstellung. theme.js steht bewusst OHNE defer und vor dem Inhalt: es setzt die Farbklasse, bevor gezeichnet wird. Mit defer blitzte beim Laden kurz die helle Fassung auf. Eine eigene Datei statt eines <script>-Blocks, weil die CSP nur script-src 'self' erlaubt - inline wäre blockiert. Fusszeile zeigt Version oder Commit, gleiches Muster wie die Kantone-App: die CI reicht beides als Build-Argument durch, lokal erscheint der Build-Zeitpunkt. Der ändert sich bei jedem Neubau und zeigt damit sofort, ob wirklich der neue Stand läuft. Beim Ergänzen der Dunkel-Varianten hatte mein Ersetzer die eben eingefügten dark:-Klassen selbst nochmal getroffen und daraus Paare wie "dark:text-slate-300 dark:text-slate-600" gemacht - bereinigt. 81 Tests, alle grün. Neu darunter: die Fusszeile zeigt Tag bzw. Commit, theme.js steht vor <body> und ohne defer, der Umschalter ist da. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o |
||
|
|
75b95b38e6 |
Phase 4: Oberfläche mit Galerie, Erfassung und Reservierung
Jinja2 mit HTMX, Tailwind über die Standalone-Binärdatei im Docker-Build - kein Node.js, kein npm. Mobile-First, Tippflächen mindestens 44 px hoch. Seiten: Galerie mit Filterleiste, Detailseite, Erfassungsformular, Stapel-Import mit Entwurfsliste, Anmeldung. HTMX tauscht beim Filtern nur die Liste aus statt die ganze Seite neu zu laden. Aus den Entscheidungen im Plan folgt konkret: - Der Stapel-Import steht ZUERST im Formular: wer zwanzig Teile hat, will nicht zwanzigmal ein Formular ausfüllen. Bewusst ohne capture-Attribut, damit sich wahlweise fotografieren oder aus der Mediathek wählen lässt - mit capture ginge nur je ein Foto pro Vorgang. - Grösse, Kategorie, Geschlecht, Saison und Zustand werden vom zuletzt gespeicherten Teil übernommen. Beim Abarbeiten einer Kiste sind das fünf Felder weniger pro Stück. - Nach dem Nachtragen eines Entwurfs geht es direkt zum nächsten, nicht auf die Detailseite - das ist der eigentliche Arbeitsablauf. - Titel bleibt leer erlaubt; angezeigt wird dann Kategorie und Grösse. - reserved_by erscheint öffentlich nur als "reserviert". Den Namen sieht ausschliesslich der angemeldete Betreiber. - Nach dem Reservieren wird der Freigabe-Link angezeigt, als vollständige Adresse zum Aufbewahren. - Post/Redirect/Get überall, damit ein Neuladen nicht erneut abschickt. - CSRF-Token in jedem Formular, zusätzlich zu SameSite=Lax. HTMX liegt lokal im Repository, nicht vom CDN: die CSP erlaubt nur script-src 'self', und ein CDN-Aufruf gäbe die IP jedes Besuchers weiter - dieselbe Überlegung wie bei Swagger und bei den Schriften der Kantone-App. Ein Test prüft für alle Seiten, dass nichts von aussen geladen wird. Nebenbei TemplateResponse auf die aktuelle Signatur umgestellt (64 Deprecation-Warnungen weniger). 77 Tests, alle grün. Zusätzlich der ganze Weg gegen den Container: anmelden, drei Fotos als Stapel, Entwurf nachtragen, Werte werden für das nächste Teil übernommen, als Gast reservieren, Name bleibt verborgen, Selbst-Freigabe über den Link. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o |
||
|
|
01cd0a73e8 |
Weisse Seite beheben: Swagger lokal ausliefern, Startseite ergänzen
Zwei Fehler, beide erst im Browser sichtbar: 1. /docs blieb weiss. FastAPI holt Swagger von cdn.jsdelivr.net, und die in Phase 3 eingeführte CSP (script-src 'self') blockiert das zu Recht. Die Dateien liegen jetzt unter app/static/swagger/ im Repository - das funktioniert auch ohne Internet und ruft keinen fremden Server auf, aus demselben Grund, aus dem die Kantone-App ihre Schriften lokal ausliefert. Das Favicon holte FastAPI ebenfalls von aussen; auch das ist jetzt lokal. 2. Die Wurzel / lieferte ein nacktes 404-JSON - im Browser sieht das aus wie eine kaputte Anwendung. Jetzt eine schlichte Platzhalter-Seite, bis die Galerie in Phase 4 steht. Ausserdem beim Testen aufgefallen: eine lokale .env-Datei reicht ADMIN_PASSWORD auch dann herein, wenn die Umgebungsvariable gelöscht wird - bei pydantic-settings haben Umgebungsvariablen Vorrang, ein leerer Wert überstimmt die Datei, ein Löschen nicht. Der Test setzt jetzt leer statt zu löschen; im Betrieb ist das dieselbe Falle. Der neue Test prüft, dass /docs keinen einzigen Verweis auf einen fremden Host enthält - genau das würde sonst unbemerkt wieder hereinrutschen. 61 Tests, alle grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o |
||
|
|
99ba74330c |
Phase 3: Anmeldung, Rate-Limits und Sicherheits-Header
Lesen darf jeder, schreiben nur der Betreiber - mit der bewussten Ausnahme des Reservierens. Dazu Dockerfile und Compose-Datei, damit sich das lokal ausprobieren lässt. Beim Bauen sind zwei Fehler aufgefallen, die ohne Test nicht aufgefallen wären: 1. slowapi zählt pro URL-Pfad. Weil jedes Kleidungsstück eine eigene URL hat, bekam jedes seinen eigenen Zähler - ein Skript hätte also den gesamten Bestand reservieren können, ohne je an ein Limit zu stossen. Genau der Missbrauch, gegen den das Limit gedacht ist. Behoben mit shared_limit und festem scope. 2. Der erste Anlauf des Tests machte fünf Anfragen gegen ein Limit von fünf und konnte damit gar nichts zeigen. Geprüft wird jetzt der tatsächlich ausgelieferte Standardwert, mit mehr Anfragen als erlaubt. Weiter umgesetzt: - Passwort als bcrypt-Hash aus der Umgebung, einmal beim Start gebildet und gemerkt. Bei jeder Anfrage neu gehasht liesse sich die Anwendung sonst ohne Anmeldung lahmlegen - bcrypt ist absichtlich langsam. - Ohne hinterlegtes Passwort bleibt der Erfassungsbereich gesperrt (503) statt offen zu stehen. Kein mitgeliefertes Standardpasswort. - Sitzung als signiertes Cookie, HttpOnly, SameSite=Lax (blockt seitenfremde POSTs), Secure abschaltbar nur fürs lokale Testen, Abmeldung nach zwei Stunden Ruhe. - CSP mit script-src 'self', nosniff, frame-ancestors none, dazu noindex und robots.txt: die Galerie ist frei zugänglich, soll aber nicht dauerhaft im Suchindex stehen. - Der Betreiber darf Reservierungen ohne Token aufheben, damit sich eine Missbrauchswelle aufräumen lässt. - uvicorn mit --proxy-headers: hinter einem Reverse-Proxy zählte sonst alles auf dessen IP, und ein einzelner Besucher sperrte alle aus. - Container läuft nicht als root; Code gehört root, nur Daten und Bilder dem Dienstbenutzer. Umgebungsvariablen heissen jetzt ausdrücklich englisch (ADMIN_PASSWORD, SECRET_KEY, ...), passend zur Anleitung und zur Kantone-App. 59 Tests, alle grün. Zusätzlich gegen den laufenden Container geprüft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o |
||
|
|
1d122b33f9 |
Phase 2: Bild-Upload, WebP-Konvertierung und Stapel-Import
Uploads werden geprüft, nach WebP gewandelt (max. 1200 px Breite) und über eine eigene Route ausgeliefert. Dazu der Stapel-Import: viele Fotos auf einmal hochladen, pro Foto entsteht ein Entwurf. Der Upload ist die grösste Angriffsfläche der Anwendung - die Galerie ist frei zugänglich, und hier landet fremdes Binärmaterial auf der Platte. Entsprechend: - Der Dateiname wird selbst erzeugt (UUID + .webp), nie aus dem Upload übernommen. Beim Ausliefern wird er zusätzlich gegen ein Muster geprüft: er kommt dort aus der URL und ist damit Nutzereingabe, auch wenn wir ihn ursprünglich vergeben haben. - Der Inhalt entscheidet, nicht die Endung - Pillow muss das Bild öffnen können. Eine als .jpg getarnte PHP-Datei fällt durch. - Pixelmasse ausdrücklich begrenzt, VOR dem Dekodieren. Pillow allein wirft erst bei der doppelten Zahl einen Fehler; dazwischen gibt es nur eine Warnung, die niemand sieht. - Gespeichert wird nur das Ergebnis. Das entfernt eingebettete Fremdinhalte und die EXIF-Daten - bei Handyfotos oft samt GPS-Koordinaten der Wohnung. Die Drehung wird vorher auf die Bildpunkte angewendet, sonst lägen Hochkant-Fotos quer. - Bilder liegen nicht unter static/, sondern werden mit festem Content-Type und nosniff ausgeliefert. HEIC wird unterstützt (pillow-heif): iPhones nehmen so auf, und ohne das fiele beim Stapel-Import jedes iPhone-Foto durch. Ein unlesbares Bild lässt bewusst den ganzen Stapel scheitern - bei zwanzig Fotos fiele sonst niemandem auf, dass eines fehlt. Beim Löschen werden die Dateien mitentfernt (ON DELETE CASCADE räumt nur die Zeilen weg), und wird das Vorschaubild gelöscht, rückt das nächste nach. 42 Tests, alle grün. Zusätzlich von Hand gegen die laufende API geprüft: 3000x2200-Foto kommt als WebP 1200x880 an, Pfadmanipulation und getarnte Dateien werden abgewiesen, Entwürfe bleiben aus der Galerie draussen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o |
||
|
|
3d7b7c706a |
Phase 1: Datenmodell, CRUD-API und Stammdaten
FastAPI mit SQLAlchemy und SQLite, Migrationen über Alembic. Die
Endpunkte aus projekt.md für items, categories und sizes stehen samt
Reservierung; Bilder und Anmeldung folgen in Phase 2 und 3.
Umgesetzt wie in Plan.md festgelegt:
- Feste Wertelisten als Enum UND als CHECK in der Datenbank. Die
CHECK-Bedingung ist der eigentliche Schutz: an SQLAlchemy vorbei (Import,
sqlite3 von Hand) käme sonst "Gril" durch, und die Filter griffen still
nicht mehr.
- Status "draft" für den Stapel-Import. Entwürfe erscheinen weder in der
Galerie noch in GET /items, solange nicht ausdrücklich status=draft
angefragt wird, und lassen sich nicht reservieren.
- Titel ist freiwillig und wird sonst beim ANZEIGEN aus Kategorie und
Grösse gebildet ("Jacken 98/104") - nicht beim Speichern, damit er einer
späteren Korrektur der Grösse folgt.
- reservation_token: nur wer es hat, kann die eigene Reservierung aufheben.
Verglichen mit compare_digest, gelöscht beim Freigeben und beim
Erledigen, damit ein alter Link nicht später eine fremde Reservierung
aufhebt.
- Eigene sizes-Tabelle mit sort_order statt SELECT DISTINCT: sonst stünde
"104" vor "56" und jeder Tippfehler würde zur Filteroption.
- Pagination auf GET /items, in projekt.md nicht vorgesehen.
Zwei SQLite-Eigenheiten, die leicht untergehen: foreign_keys ist
standardmässig AUS (ohne PRAGMA greift ON DELETE CASCADE nicht), und
check_same_thread muss für FastAPI abgeschaltet werden. Beides in
database.py, dazu WAL fürs gleichzeitige Lesen.
24 Tests, alle grün. Zusätzlich von Hand geprüft: Migration anwenden,
Eintrag anlegen, reservieren, ohne Token freigeben (403), mit Token
freigeben (200), Swagger UI erreichbar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
|