commit d5bb83a94f20e7899a9cf77967daf2528dd6cea6 Author: Stefan Date: Sat Aug 29 21:03:10 2026 +0200 Projektbeschrieb und Umsetzungsplan projekt.md beschreibt die Kinderkleider-Börse (FastAPI, SQLite, Bild-Upload mit WebP-Konvertierung). Plan.md ergänzt die dort offenen Entscheidungen: Frontend-Variante A (Jinja2 + HTMX, kein Node.js), Erfassen hinter Passwort, Reservieren ohne Anmeldung, erreichbar aus dem Internet. Aus dieser Kombination folgt der Schwerpunkt des Plans - ein offener, zustandsändernder Endpunkt im Internet und ein Bild-Upload sind die beiden Angriffsflächen, die den Ausschlag geben. Noch kein Code. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6386c1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Zugangsdaten - gehören nie ins Repository +.env + +# Laufzeitdaten: liegen im Betrieb in Docker-Volumes, nicht im Code +data/ +uploads/ +*.sqlite +*.sqlite3 +*.db + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ + +# Erzeugtes CSS (entsteht beim Docker-Build aus der Tailwind-CLI) +app/static/style.css +tailwindcss + +# Editor / Betriebssystem +.vscode/ +.idea/ +*.swp +.DS_Store +Thumbs.db diff --git a/Plan.md b/Plan.md new file mode 100644 index 0000000..a2df5f4 --- /dev/null +++ b/Plan.md @@ -0,0 +1,229 @@ +# Umsetzungsplan – Kinderkleider-Börse + +Grundlage: `projekt.md`. Dieser Plan ergänzt sie um die Entscheidungen, die dort +offen waren, und um die Punkte, die durch den Internet-Zugang dazukommen. + +## Getroffene Entscheidungen + +| Frage | Entscheidung | +|---|---| +| Frontend | **Variante A** – Jinja2 + HTMX + Tailwind, ein einziger Dienst, kein Node.js | +| Erfassen/Bearbeiten/Löschen | Hinter **einem Passwort** | +| Reservieren | **Ohne Anmeldung** | +| Erreichbarkeit | **Aus dem Internet**, per Reverse-Proxy mit HTTPS | + +Die letzten beiden zusammen sind der eigentliche Knackpunkt dieses Projekts: +ein Endpunkt, den jeder im Internet ohne Anmeldung auslösen kann, der den +Zustand ändert. Alles unter „Absicherung" ergibt sich daraus. + +--- + +## Architektur + +Ein Container, ein Prozess (`uvicorn`), eine SQLite-Datei. FastAPI liefert +sowohl die REST-API (`/api/v1/...`) als auch die gerenderten Seiten aus – +dieselbe Anwendung, dieselben CRUD-Funktionen. HTMX ersetzt beim Filtern und +Reservieren nur Teilbereiche der Seite, statt eine SPA zu bauen. + +**Tailwind ohne Node.js:** nicht per CDN (im Betrieb nicht vorgesehen und wäre +eine Fremdverbindung), sondern über die **Standalone-CLI** – eine einzelne +Binärdatei, die im Docker-Build einmalig ein fertiges `style.css` erzeugt. Im +Image landet nur das fertige CSS. + +**Persistenz:** ein Volume für die Datenbank, ein zweites für die Bilder. Beide +ausserhalb des ausgelieferten Codes. + +### Projektstruktur + +Die Struktur in `projekt.md` ist an einer Stelle verrutscht (die Module stehen +auf derselben Ebene wie `app/`, `static/` und `templates/` hängen an einer +Ebene, die es nicht gibt). Vorschlag: + +```text +kleiderboerse/ +├── app/ +│ ├── __init__.py +│ ├── main.py # FastAPI-App, Startup, Router einhängen +│ ├── config.py # Einstellungen aus Umgebungsvariablen +│ ├── database.py # Engine & Session +│ ├── models.py # SQLAlchemy-Modelle +│ ├── schemas.py # Pydantic-Schemas +│ ├── crud.py # Datenbankzugriffe +│ ├── security.py # Passwort, Sitzung, CSRF, Rate-Limit +│ ├── images.py # Upload-Prüfung, WebP-Konvertierung +│ ├── routers/ +│ │ ├── items.py # /api/v1/items +│ │ ├── categories.py # /api/v1/categories, /sizes +│ │ └── pages.py # HTML-Seiten (Galerie, Detail, Erfassen) +│ ├── static/ # erzeugtes CSS, Icons (Bilder NICHT hier) +│ └── templates/ +├── alembic/ # Migrationen +├── data/ # Volume: kleiderboerse.sqlite +├── uploads/ # Volume: hochgeladene Bilder +├── tests/ +├── Dockerfile +├── docker-compose.yml +├── requirements.txt +└── README.md +``` + +Hochgeladene Bilder liegen **nicht** unter `static/`, sondern in einem eigenen +Verzeichnis, das über eine eigene Route ausgeliefert wird. Sonst wäre jede +hochgeladene Datei unter ihrem Namen direkt abrufbar – und ein als Bild +getarntes Skript ebenfalls. + +--- + +## Datenmodell – Abweichungen von `projekt.md` + +Das Schema wird grundsätzlich übernommen. Vier Ergänzungen: + +1. **Feste Wertelisten statt freier VARCHAR.** `gender`, `season`, `condition` + und `status` bekommen ein Python-Enum plus `CHECK`-Bedingung in der + Datenbank. Ohne das schleicht sich über die API früher oder später + `"Gril"` oder `"reserviert"` ein, und die Filter greifen dann still nicht + mehr. + +2. **`reservation_token` (neu) in `items`.** Zufälliger Wert, der beim + Reservieren erzeugt wird. Nur wer ihn hat, kann die eigene Reservierung + wieder aufheben. Begründung unten unter „Offene Punkte". + +3. **`sizes` als eigene Tabelle.** `projekt.md` sieht `GET /api/v1/sizes` vor, + aber keine Tabelle dazu. Grössen aus den vorhandenen Einträgen zu + destillieren (`SELECT DISTINCT size`) klingt bequem, sortiert aber falsch + (`98/104` vor `24`) und liefert Tippfehler gleich mit als Filteroption. + Darum eine gepflegte Liste mit `sort_order`. + +4. **`updated_at`** wird in der Anwendung gesetzt, nicht per DB-Trigger – + bleibt so unabhängig von SQLite-Eigenheiten. + +Ausserdem: beim Löschen eines Eintrags müssen die **Bilddateien mitgelöscht** +werden, sonst wächst das Upload-Verzeichnis unbegrenzt mit verwaisten Dateien. +`ON DELETE CASCADE` räumt nur die Datenbankzeilen weg, nicht die Dateien. + +--- + +## Absicherung + +Die App ist aus dem Internet erreichbar, also gelten hier andere Massstäbe als +bei einem Heimnetz-Dienst. Punkte 1 und 2 sind die wichtigsten. + +### 1. Bild-Upload – die grösste Angriffsfläche + +- Dateiname **nie** aus dem Upload übernehmen, sondern selbst erzeugen + (UUID + `.webp`). Damit ist Pfad-Manipulation ausgeschlossen. +- Vor dem Öffnen: Grösse begrenzen (z.B. 10 MB) und Inhaltstyp prüfen. +- `Image.MAX_IMAGE_PIXELS` setzen – sonst kann ein wenige Kilobyte grosses + PNG beim Entpacken den Arbeitsspeicher füllen („Dekompressionsbombe"). +- Nach dem Konvertieren nach WebP wird **nur das Ergebnis** gespeichert, nie + das Original. Das entfernt eingebettete Fremdinhalte und die EXIF-Daten – + letztere enthalten bei Handyfotos oft die **GPS-Koordinaten der Wohnung**. +- Ausliefern mit festem `Content-Type: image/webp` und + `X-Content-Type-Options: nosniff`. + +### 2. Reservieren ohne Anmeldung + +Ein offener, zustandsändernder Endpunkt im Internet. Ohne Bremse kann ein +Skript in Sekunden alles reservieren oder `reserved_by` als Werbefläche +missbrauchen. + +- **Rate-Limit pro IP** (`slowapi`), z.B. 5 Reservierungen pro Stunde. +- `reserved_by` begrenzen (Länge, keine URLs) und beim Anzeigen maskieren. + Jinja2 maskiert von sich aus – entscheidend ist, `|safe` dort nirgends zu + verwenden. +- Reservierungen sind **jederzeit vom Betreiber aufhebbar** (Admin-Ansicht), + damit eine Missbrauchswelle in einem Rutsch aufgeräumt werden kann. + +### 3. Anmeldung fürs Erfassen + +- Passwort als **bcrypt-Hash** aus einer Umgebungsvariablen. Kein + Standardpasswort mitliefern: ohne gesetzten Wert ist der Bereich gesperrt, + nicht offen. +- Hash **einmal** beim Start berechnen, nicht pro Anfrage. +- Sitzungs-Cookie `HttpOnly`, `SameSite=Lax`, `Secure`. +- **`Secure` muss `X-Forwarded-Proto` auswerten**, nicht nur die direkte + Verbindung – hinter einem TLS-Reverse-Proxy sieht die App sonst „HTTP" und + lässt das Cookie unverschlüsselt mitgehen. +- Rate-Limit auch auf die Anmeldung, plus Sitzungs-Timeout. +- CSRF-Token für alle ändernden Formulare. + +### 4. Allgemein + +- `Content-Security-Policy` mit `script-src 'self'` (HTMX wird lokal + ausgeliefert, nicht per CDN), dazu `nosniff` und `frame-ancestors 'none'`. +- HTTPS erzwingen; `uvicorn` mit `--proxy-headers` und gesetzten + `forwarded-allow-ips`. +- Fehlermeldungen ohne interne Details; Stacktraces nur ins Log. +- Pagination auf `GET /api/v1/items` – ohne Begrenzung liefert der Endpunkt + irgendwann alle Einträge samt Bildpfaden in einer Antwort. + +### 5. Personendaten + +`reserved_by` ist ein Personenname auf einer öffentlich erreichbaren Seite. +Vorschlag: auf der Galerie nur „reserviert" anzeigen, den Namen ausschliesslich +in der Admin-Ansicht. Zusätzlich beim Erledigen (`mark-given`) den Namen +löschen. Das ist datensparsam und macht die Seite gleichzeitig unattraktiver +für Spam. + +--- + +## Meilensteine + +Die Phasen aus `projekt.md`, ergänzt um Tests und die Absicherung. + +**Phase 1 – Fundament** +- Projektgerüst, `requirements.txt`, Einstellungen aus Umgebungsvariablen +- SQLAlchemy-Modelle, Alembic-Erstmigration, Kategorien und Grössen vorbefüllen +- CRUD-Schicht mit Tests +- `GET`/`POST`/`PATCH`/`DELETE /api/v1/items`, `GET /api/v1/categories`, `/sizes` +- Prüfpunkt: über Swagger UI ein Kleidungsstück anlegen, filtern, ändern, löschen + +**Phase 2 – Bilder** +- Upload per `multipart/form-data`, mehrere Bilder pro Eintrag +- Prüfung und WebP-Konvertierung (max. 1200 px Breite) nach Abschnitt 1 oben +- Ausliefer-Route, `is_primary`-Logik, Aufräumen beim Löschen +- Prüfpunkt: bewusst fehlerhafte Uploads (zu gross, falscher Typ, riesige + Pixelmasse, `.php` als `.jpg` getarnt) werden alle abgewiesen + +**Phase 3 – Absicherung** +- Anmeldung, Sitzung, CSRF, Rate-Limits, Sicherheits-Header +- Prüfpunkt: ohne Anmeldung ist kein Schreibzugriff möglich; ohne gesetztes + Passwort ist der Erfassungsbereich gesperrt + +**Phase 4 – Oberfläche** +- Mobile-First-Layout, Galerie mit Filterleiste (HTMX) +- Detailseite, Erfassungsformular mit Kamera-Aufnahme + (``) +- Reservierungs-Ablauf inklusive Selbst-Freigabe +- Prüfpunkt: der ganze Weg auf einem echten Smartphone + +**Phase 5 – Betrieb** +- `Dockerfile` (Tailwind-Build inbegriffen), `docker-compose.yml`, Volumes +- Healthcheck, Backup-Hinweise für Datenbank und Bilder +- README für Einrichtung und Reverse-Proxy + +--- + +## Offene Punkte + +1. **Wer darf eine Reservierung wieder aufheben?** `projekt.md` sieht + `POST /items/{id}/release` vor, sagt aber nicht, wer ihn aufrufen darf. Ist + er offen, kann jeder die Reservierung eines anderen löschen. Vorschlag: beim + Reservieren wird ein Token vergeben und als Link angezeigt („Reservierung + aufheben") – wer ihn hat, kann die eigene zurücknehmen; der Betreiber kann + es ohnehin. Bitte bestätigen, dann kommt `reservation_token` ins Modell. + +2. **Soll die Galerie überhaupt öffentlich sein?** Auch ohne Schreibzugriff + zeigt sie Kinderkleidung, Namen und indirekt den Wohnort. Ein einfacher + Zugangscode für die ganze Seite (einmal eingeben, bleibt im Cookie) würde + Suchmaschinen und Zufallsbesucher aussperren, ohne dass jemand ein Konto + braucht. Empfehlung: ja, zusätzlich zu `noindex`. Deine Entscheidung. + +3. **Benachrichtigung bei Reservierung?** Nicht in `projekt.md`. Ohne sie musst + du selbst nachschauen. Ein einfacher Weg wäre eine Nachricht per E-Mail oder + Telegram. Kann auch später kommen – dann aber besser gleich als eigener + Meilenstein statt nachträglich eingeschoben. + +4. **Mehrere Kinder / Grössenverläufe?** Aktuell ist alles ein flacher Bestand. + Falls du später nach „von wem" oder „Jahrgang" filtern willst, wäre jetzt + der günstige Zeitpunkt für ein zusätzliches Feld. diff --git a/projekt.md b/projekt.md new file mode 100644 index 0000000..48a5cf2 --- /dev/null +++ b/projekt.md @@ -0,0 +1,129 @@ +# 📦 Kinderkleider-Börse (Web-App) + +Eine schlanke, mobile-optimierte Web-App zum Katalogisieren, Präsentieren und Reservieren von zu klein gewordener Kinderkleidung für Familie, Freunde und Nachbarn. + +--- + +## 🚀 Zielsetzungs & Kern-Features + +- **Schnelles Erfassen (Mobile First):** Foto direkt mit dem Smartphone aufnehmen, Grösse & Kategorie wählen, fertig. +- **Übersichtliche Galerie & Filter:** Schnelle Filterung nach Konfektionsgrösse, Kategorie, Geschlecht und Saison. +- **Einfaches Reservierungssystem:** Interessierte können Kleidungsstücke mit einem Klick reservieren. +- **Bildoptimierung:** Automatische Skalierung und WebP-Komprimierung beim Upload, um Speicherplatz und Bandbreite zu sparen. + +--- + +## 🛠 Technologiestack (Python-basiert) + +- **Backend:** Python 3.11+ mit [FastAPI](https://fastapi.tiangolo.com/) (hohe Performance, automatische OpenAPI/Swagger-Dokumentation) +- **Datenbank & ORM:** SQLite mit [SQLAlchemy](https://www.sqlalchemy.org/) & [Alembic](https://alembic.sqlalchemy.org/) (Migrations) +- **Bildverarbeitung:** Pillow (PIL) für WebP-Konvertierung und Resizing +- **Frontend (Optionen):** + - *Variante A (Single File / Monolith):* Jinja2 Templates + HTMX + Tailwind CSS (sehr schnell zu entwickeln, kein Node.js Build-Step nötig) + - *Variante B (Decoupled):* Vue.js / React SPA (greift auf die REST-API zu) +- **Deployment:** Docker & Docker Compose (leicht auf Unraid / Server auszuführen) + +--- + +## 🗄 Datenbank-Schema + +### 1. `categories` +| Feld | Typ | Beschreibung | +|---|---|---| +| `id` | INTEGER (PK) | Eindeutige ID | +| `name` | VARCHAR(50) | Name (z. B. Hosen, Jacken, Schuhe, Pullover) | +| `slug` | VARCHAR(50) | URL-freundlicher Name | + +### 2. `items` (Haupttabelle) +| Feld | Typ | Beschreibung | +|---|---|---| +| `id` | INTEGER / UUID (PK) | Eindeutige ID | +| `title` | VARCHAR(100) | Kurzer Titel (z. B. "Warme Winterjacke rot") | +| `description` | TEXT | Optionale Details oder Mängel | +| `size` | VARCHAR(20) | Konfektionsgrösse (z. B. `98/104`, `24`, `80`) | +| `gender` | VARCHAR(10) | `boy`, `girl`, `unisex` | +| `season` | VARCHAR(15) | `spring_summer`, `autumn_winter`, `all_year` | +| `condition` | VARCHAR(20) | `new`, `very_good`, `good`, `worn` | +| `status` | VARCHAR(20) | `available`, `reserved`, `given_away` | +| `category_id` | FK -> `categories.id` | Zuordnung zur Kategorie | +| `reserved_by` | VARCHAR(100) | Name/Kontakt der Person, die es reserviert hat | +| `created_at` | TIMESTAMP | Erstellungsdatum | +| `updated_at` | TIMESTAMP | Letztes Update | + +### 3. `item_images` +| Feld | Typ | Beschreibung | +|---|---|---| +| `id` | INTEGER / UUID (PK) | Eindeutige ID | +| `item_id` | FK -> `items.id` | Referenz zum Kleidungsstück | +| `image_url` | VARCHAR(255) | Pfad zur Bilddatei (z. B. `/static/uploads/img_1234.webp`) | +| `is_primary` | BOOLEAN | Hauptbild für Galerie-Vorschau | + +--- + +## 🔌 API-Endpunkte (FastAPI REST) + +### Kleidung verwalten & durchsuchen +- `GET /api/v1/items` + - **Query-Params:** `?size=98/104&category_id=2&status=available&gender=unisex` + - **Response:** Liste gefilterter Kleidungsstücke mit Primärbild. +- `GET /api/v1/items/{id}` + - **Response:** Einzelnes Kleidungsstück inklusive aller Bilder und Details. +- `POST /api/v1/items` + - **Content-Type:** `multipart/form-data` + - **Body:** JSON-Daten + Bild-Uploads. +- `PATCH /api/v1/items/{id}` + - **Body:** Partial Update für Status, Titel, Beschreibung etc. +- `DELETE /api/v1/items/{id}` + - **Description:** Kleidungsstück und zugehörige Bilder löschen. + +### Reservierung & Status +- `POST /api/v1/items/{id}/reserve` + - **Body:** `{"reserved_by": "Familie Meier"}` +- `POST /api/v1/items/{id}/release` + - Setzt Status zurück auf `available` und löscht `reserved_by`. +- `POST /api/v1/items/{id}/mark-given` + - Setzt Status auf `given_away`. + +### Stammdaten +- `GET /api/v1/categories` +- `GET /api/v1/sizes` + +--- + +## 📂 Empfohlene Projekt-Struktur + +```text +kinderkleider-app/ +├── app/ +├── database.py # DB-Engine & Session Setup +├── models.py # SQLAlchemy Modelle +├── schemas.py # Pydantic Schemas (Request/Response validation) +├── crud.py # Datenbank-Zugriffslogik +├── main.py # FastAPI App Entrypoint & Routen +├── utils.py # Bild-Komprimierung & Helper +│ ├── static/ # Uploaded Images & CSS +│ └── templates/ # Jinja2 Templates (falls HTML direkt gerendert wird) +├── Dockerfile +├── docker-compose.yml +├── requirements.txt +└── README.md +``` + +--- + +## 📋 Meilensteine / Roadmap + +- [ ] **Phase 1: Backend Basic MVP** + - [ ] FastAPI Setup mit SQLite & SQLAlchemy + - [ ] Datenmodelle & Migrationen anlegen + - [ ] CRUD Endpunkte für `items` und `categories` testen (Swagger UI) +- [ ] **Phase 2: Bild-Handling & Speicher** + - [ ] `multipart/form-data` Upload via FastAPI + - [ ] Pillow-Integration zur Konvertierung aller Uploads nach WebP (max. 1200px Breite) +- [ ] **Phase 3: Frontend Setup** + - [ ] Mobile-First Layout mit Tailwind CSS + - [ ] Filterleiste für Grösse, Kategorie & Status + - [ ] Reservierungs-Modal / Button +- [ ] **Phase 4: Containerisierung & Deployment** + - [ ] `Dockerfile` & `docker-compose.yml` schreiben + - [ ] Persistence per Volume-Mount für Datenbank & Bild-Uploads