#94 — Vorratsschrank — Lagerbestand mit Barcode-Scan (Phase 1) #94

Closed
opened 2026-08-18 13:13:54 +02:00 by lena · 3 comments
lena commented 2026-08-18 13:13:54 +02:00 (Migrated from git.butzei.de)

Story: Vorratsschrank — Lagerbestand mit Barcode-Scan (Phase 1)

Depends on: #89 (Listentyp-Vereinheitlichung — Vorratsschrank ist der sechste Listentyp darin, ohne
Erledigt-/Abhak-Workflow; siehe #89's aktualisierte Typenliste).

As a Haushaltsmitglied,
I want to den Lagerbestand unseres Vorratsschranks per Barcode-Scan beim Ein-/Auslagern pflegen, Soll-Mengen
definieren und die Einkaufsliste automatisch aktualisieren lassen,
so that wir jederzeit wissen, was vorrätig ist, nie unbemerkt etwas ausgeht und nichts doppelt gekauft wird.

Acceptance criteria:

Barcode-Scan & Ein-/Auschecken

  • Barcode-Scan per Geräte-Kamera (Handy/Laptop) direkt im Browser, keine Zusatz-Hardware für Phase 1
    nötig.
  • Zwei Aktionen: Einchecken (Bestand +1) und Auschecken (Bestand −1) je Scan.
  • Bekannter Barcode → automatisch identifiziert, Bestand wird direkt angepasst, keine Rückfrage.
  • Architect-Hinweis: Ein-/Auschecken darf nicht als simples Lese-Ändere-Schreibe (Read-Modify-Write) auf
    den aktuellen Bestandswert implementiert werden. Da der Bestand haushaltsweit geteilt ist und mehrere
    Personen gleichzeitig scannen können (das ist der zentrale Zweck dieser Story), braucht es eine atomare
    Erhöhung/Verringerung auf DB-Ebene (z. B. UPDATE ... SET quantity = quantity + 1), sonst gehen bei
    zeitgleichen Scans Änderungen verloren — ein Bug-Muster, das in diesem Projekt bereits mehrfach erst
    nachträglich in der Code-Review gefunden wurde (siehe Decisions Log #61/#71/#80/#85), hier von vornherein
    vermeiden.
  • Unbekannter Barcode (erstmaliges Scannen im Haushalt):
    • App fragt zuerst eine öffentliche Produktdatenbank (z. B. Open Food Facts) per Barcode ab.
    • Gefunden → Produktname wird vorausgefüllt, Nutzer bestätigt/korrigiert.
    • Nicht gefunden → Nutzer gibt den Produktnamen manuell per Text ein (Foto/Sprache folgt in #95).
    • Ergebnis wird dauerhaft mit dem Barcode verknüpft gespeichert (nie zweimal gefragt) — erweitert dieselbe
      globale Produkt-Wissensdatenbank wie #90, statt eine zweite aufzubauen. Konkret: der Barcode wird als
      optionales Zusatzfeld an einen bestehenden (namensbasierten) Produkt-Eintrag angehängt, statt eine zweite,
      barcode-basierte Produktidentität parallel zu führen (siehe #90's entsprechende Ergänzung).

Soll-Mengen & Einkaufslisten-Integration

  • Jedes Vorratsschrank-Produkt kann eine optionale Soll-Menge (Mindestbestand) haben.
  • Beim Anlegen eines Vorratsschranks wählt der Nutzer eine feste Ziel-Einkaufsliste aus. Fällt der Bestand
    durch ein Auschecken unter die definierte Soll-Menge, wird die fehlende Menge automatisch auf genau diese
    Liste geschrieben (kein Griff nach dem persönlichen Default aus #85 — der Vorratsschrank ist
    haushaltsweit geteilt, ein persönlicher Default wäre mehrdeutig, wenn Haushaltsmitglieder
    unterschiedliche Defaults haben). Der Eintrag nutzt dasselbe geteilte "Quelle: manuell/synchronisiert/
    Vorratsschrank"-Datenmodell, das #90 für den Online-Warenkorb-Abgleich vorsieht — keine zwei getrennten
    "automatische Quelle"-Mechanismen.
  • Wird ein Produkt manuell auf die Einkaufsliste geschrieben, das bereits ausreichend im Vorratsschrank
    vorhanden ist, erscheint ein Hinweis mit Vorschlag zur Mengenanpassung, z. B.: "Du hast noch 3 Dosen
    Kichererbsen im Schrank. Da du mindestens 1 Dose vorrätig haben möchtest, nur 3 statt 5 auf die Liste
    schreiben?" — Nutzer kann den Vorschlag annehmen oder die ursprüngliche Menge behalten.

Übersicht

  • Eine Ansicht zeigt jederzeit den aktuellen Gesamtbestand (Produkt, Menge, Barcode).

Feature-Umfang (Anwendung der bestehenden Todo-Item-Features auf Vorratsschrank-Artikel)

  • Fälligkeitsdatum: Nein — Bestandsartikel haben keine Fälligkeit; Haltbarkeitsdaten sind laut Scope
    oben ausdrücklich nicht Teil dieser Story.
  • Priorität (Low/Normal/High): Nein — die Soll-Menge liefert bereits ein objektives
    Dringlichkeitssignal; eine zusätzliche manuelle Einstufung wäre redundant.
  • Zuweisung an ein Mitglied: Nein — es gibt keine abzuarbeitende Einzelaufgabe pro Artikel, nur einen
    geteilten Bestand.
  • Manuelle Reihenfolge (Drag & Drop): Ja — die Bestandsübersicht soll frei sortierbar sein, analog zur
    Kategorien-Sortierung aus #80.
  • Subtasks/Checkliste: Nein — ein Artikel ist eine Zahl (Bestand), kein mehrstufiger Vorgang.
  • Labels/Tags: Ja — nützlich zum Gruppieren, z. B. "vegan", "Bio", "Notvorrat".
  • Wiederkehrende Todos: Nein — Artikel werden nicht "erledigt", sondern laufend verbraucht/nachgefüllt;
    das übernimmt bereits das Soll-Mengen-System oben.
  • Kommentare: Ja — z. B. "Bio-Variante schmeckt besser" oder "bei Händler X günstiger".
  • Aktivitäts-Feed pro Liste: Ja, wichtig — bei geteiltem Haushaltsbestand ist Nachvollziehbarkeit (wer
    hat wann was ein-/ausgecheckt) besonders wertvoll.
  • E-Mail-Benachrichtigungen: Nein, bewusst nicht in dieser Story (siehe Rückmeldung des Menschen
    2026-08-07 — kein Bedarf).
  • Browser-Push-Benachrichtigungen: Nein, bewusst nicht in dieser Story (dieselbe Entscheidung).
  • Kategorien/Sektionen: Ja — Bestand gruppierbar (Tiefkühl, Konserven, Getränke …), analog zur
    Einkaufsliste.
  • CSV-Import/Export: Ja — nützlich zur Datensicherung/Übersicht des Bestands.
  • Inline-Bearbeiten (Doppelklick, #86): Ja — Produktname soll direkt korrigierbar sein wie bei Todos.
  • Overflow-"⋯"-Menü pro Item: Ja — dieselbe UI-Konvention wie bei Todos (Bearbeiten, Löschen, Soll-Menge
    setzen), statt eine neue Interaktionsform zu erfinden.

Out of scope for this story:

  • Phase 2 (Foto-/Sprach-Erkennung unbekannter Produkte) — eigene Story, siehe #95.
  • Phase 3 (Wandgerät mit Tasten/Bewegungssensor) — kein Software-Projekt dieses Repos. Phase 1 muss aber eine
    stabile, dokumentierte API für "Scan-Event: Barcode X, Ein-/Auschecken" bereitstellen, die ein späteres
    externes Gerät aufrufen kann, ohne dass umgebaut werden muss (gleiches Prinzip wie #90's
    Online-Warenkorb-Hinweis).
  • Haltbarkeitsdaten-Tracking (laut Vorgabe nur haltbare Produkte mit Barcode, keine frischen Waren — kein
    Bedarf für ein Ablaufdatum-Konzept).

Entschieden (2026-08-07):

  • Ein Vorratsschrank hat einen gemeinsamen Bestand pro Haushalt (geteilter Mitgliedergruppe) — spiegelt die
    Realität, dass es auch nur einen physischen Schrank gibt. Die Mitgliedschaft läuft über ein eigenes
    Sharing/Invite analog zu einer Liste, ist aber an dieselben Freigaben wie die verknüpfte Ziel-Einkaufsliste
    (siehe oben) gekoppelt, statt unabhängig verwaltet zu werden.
  • Die Ziel-Einkaufsliste für automatische Einträge bei Soll-Mengen-Unterschreitung wird beim Anlegen des
    Vorratsschranks fest ausgewählt (siehe oben), nicht über den persönlichen Default aus #85 ermittelt.

Zur Kenntnis:

  • Open Food Facts deckt vor allem Lebensmittel ab; bei Drogerie-/Haushaltsartikeln sind Lücken zu erwarten —
    unproblematisch, da die manuelle Eingabe als Fallback ohnehin vorgesehen ist.
# Story: Vorratsschrank — Lagerbestand mit Barcode-Scan (Phase 1) **Depends on:** `#89` (Listentyp-Vereinheitlichung — Vorratsschrank ist der sechste Listentyp darin, ohne Erledigt-/Abhak-Workflow; siehe `#89`'s aktualisierte Typenliste). **As a** Haushaltsmitglied, **I want to** den Lagerbestand unseres Vorratsschranks per Barcode-Scan beim Ein-/Auslagern pflegen, Soll-Mengen definieren und die Einkaufsliste automatisch aktualisieren lassen, **so that** wir jederzeit wissen, was vorrätig ist, nie unbemerkt etwas ausgeht und nichts doppelt gekauft wird. **Acceptance criteria:** *Barcode-Scan & Ein-/Auschecken* - [ ] Barcode-Scan per Geräte-Kamera (Handy/Laptop) direkt im Browser, keine Zusatz-Hardware für Phase 1 nötig. - [ ] Zwei Aktionen: **Einchecken** (Bestand +1) und **Auschecken** (Bestand −1) je Scan. - [ ] Bekannter Barcode → automatisch identifiziert, Bestand wird direkt angepasst, keine Rückfrage. - [ ] **Architect-Hinweis:** Ein-/Auschecken darf nicht als simples Lese-Ändere-Schreibe (Read-Modify-Write) auf den aktuellen Bestandswert implementiert werden. Da der Bestand haushaltsweit geteilt ist und mehrere Personen gleichzeitig scannen können (das ist der zentrale Zweck dieser Story), braucht es eine atomare Erhöhung/Verringerung auf DB-Ebene (z. B. `UPDATE ... SET quantity = quantity + 1`), sonst gehen bei zeitgleichen Scans Änderungen verloren — ein Bug-Muster, das in diesem Projekt bereits mehrfach erst nachträglich in der Code-Review gefunden wurde (siehe Decisions Log `#61`/`#71`/`#80`/`#85`), hier von vornherein vermeiden. - [ ] Unbekannter Barcode (erstmaliges Scannen im Haushalt): - App fragt zuerst eine öffentliche Produktdatenbank (z. B. Open Food Facts) per Barcode ab. - Gefunden → Produktname wird vorausgefüllt, Nutzer bestätigt/korrigiert. - Nicht gefunden → Nutzer gibt den Produktnamen manuell per Text ein (Foto/Sprache folgt in `#95`). - Ergebnis wird dauerhaft mit dem Barcode verknüpft gespeichert (nie zweimal gefragt) — erweitert dieselbe globale Produkt-Wissensdatenbank wie `#90`, statt eine zweite aufzubauen. Konkret: der Barcode wird als optionales Zusatzfeld an einen bestehenden (namensbasierten) Produkt-Eintrag angehängt, statt eine zweite, barcode-basierte Produktidentität parallel zu führen (siehe `#90`'s entsprechende Ergänzung). *Soll-Mengen & Einkaufslisten-Integration* - [ ] Jedes Vorratsschrank-Produkt kann eine optionale **Soll-Menge** (Mindestbestand) haben. - [ ] Beim Anlegen eines Vorratsschranks wählt der Nutzer eine feste Ziel-Einkaufsliste aus. Fällt der Bestand durch ein Auschecken unter die definierte Soll-Menge, wird die fehlende Menge automatisch auf genau diese Liste geschrieben (kein Griff nach dem persönlichen Default aus `#85` — der Vorratsschrank ist haushaltsweit geteilt, ein persönlicher Default wäre mehrdeutig, wenn Haushaltsmitglieder unterschiedliche Defaults haben). Der Eintrag nutzt dasselbe geteilte "Quelle: manuell/synchronisiert/ Vorratsschrank"-Datenmodell, das `#90` für den Online-Warenkorb-Abgleich vorsieht — keine zwei getrennten "automatische Quelle"-Mechanismen. - [ ] Wird ein Produkt manuell auf die Einkaufsliste geschrieben, das bereits ausreichend im Vorratsschrank vorhanden ist, erscheint ein Hinweis mit Vorschlag zur Mengenanpassung, z. B.: "Du hast noch 3 Dosen Kichererbsen im Schrank. Da du mindestens 1 Dose vorrätig haben möchtest, nur 3 statt 5 auf die Liste schreiben?" — Nutzer kann den Vorschlag annehmen oder die ursprüngliche Menge behalten. *Übersicht* - [ ] Eine Ansicht zeigt jederzeit den aktuellen Gesamtbestand (Produkt, Menge, Barcode). *Feature-Umfang (Anwendung der bestehenden Todo-Item-Features auf Vorratsschrank-Artikel)* - [ ] Fälligkeitsdatum: **Nein** — Bestandsartikel haben keine Fälligkeit; Haltbarkeitsdaten sind laut Scope oben ausdrücklich nicht Teil dieser Story. - [ ] Priorität (Low/Normal/High): **Nein** — die Soll-Menge liefert bereits ein objektives Dringlichkeitssignal; eine zusätzliche manuelle Einstufung wäre redundant. - [ ] Zuweisung an ein Mitglied: **Nein** — es gibt keine abzuarbeitende Einzelaufgabe pro Artikel, nur einen geteilten Bestand. - [ ] Manuelle Reihenfolge (Drag & Drop): **Ja** — die Bestandsübersicht soll frei sortierbar sein, analog zur Kategorien-Sortierung aus `#80`. - [ ] Subtasks/Checkliste: **Nein** — ein Artikel ist eine Zahl (Bestand), kein mehrstufiger Vorgang. - [ ] Labels/Tags: **Ja** — nützlich zum Gruppieren, z. B. "vegan", "Bio", "Notvorrat". - [ ] Wiederkehrende Todos: **Nein** — Artikel werden nicht "erledigt", sondern laufend verbraucht/nachgefüllt; das übernimmt bereits das Soll-Mengen-System oben. - [ ] Kommentare: **Ja** — z. B. "Bio-Variante schmeckt besser" oder "bei Händler X günstiger". - [ ] Aktivitäts-Feed pro Liste: **Ja, wichtig** — bei geteiltem Haushaltsbestand ist Nachvollziehbarkeit (wer hat wann was ein-/ausgecheckt) besonders wertvoll. - [ ] E-Mail-Benachrichtigungen: **Nein, bewusst nicht in dieser Story** (siehe Rückmeldung des Menschen 2026-08-07 — kein Bedarf). - [ ] Browser-Push-Benachrichtigungen: **Nein, bewusst nicht in dieser Story** (dieselbe Entscheidung). - [ ] Kategorien/Sektionen: **Ja** — Bestand gruppierbar (Tiefkühl, Konserven, Getränke …), analog zur Einkaufsliste. - [ ] CSV-Import/Export: **Ja** — nützlich zur Datensicherung/Übersicht des Bestands. - [ ] Inline-Bearbeiten (Doppelklick, `#86`): **Ja** — Produktname soll direkt korrigierbar sein wie bei Todos. - [ ] Overflow-"⋯"-Menü pro Item: **Ja** — dieselbe UI-Konvention wie bei Todos (Bearbeiten, Löschen, Soll-Menge setzen), statt eine neue Interaktionsform zu erfinden. **Out of scope for this story:** - Phase 2 (Foto-/Sprach-Erkennung unbekannter Produkte) — eigene Story, siehe `#95`. - Phase 3 (Wandgerät mit Tasten/Bewegungssensor) — kein Software-Projekt dieses Repos. Phase 1 muss aber eine stabile, dokumentierte API für "Scan-Event: Barcode X, Ein-/Auschecken" bereitstellen, die ein späteres externes Gerät aufrufen kann, ohne dass umgebaut werden muss (gleiches Prinzip wie `#90`'s Online-Warenkorb-Hinweis). - Haltbarkeitsdaten-Tracking (laut Vorgabe nur haltbare Produkte mit Barcode, keine frischen Waren — kein Bedarf für ein Ablaufdatum-Konzept). **Entschieden (2026-08-07):** - Ein Vorratsschrank hat einen **gemeinsamen** Bestand pro Haushalt (geteilter Mitgliedergruppe) — spiegelt die Realität, dass es auch nur einen physischen Schrank gibt. Die Mitgliedschaft läuft über ein eigenes Sharing/Invite analog zu einer Liste, ist aber an dieselben Freigaben wie die verknüpfte Ziel-Einkaufsliste (siehe oben) gekoppelt, statt unabhängig verwaltet zu werden. - Die Ziel-Einkaufsliste für automatische Einträge bei Soll-Mengen-Unterschreitung wird beim Anlegen des Vorratsschranks fest ausgewählt (siehe oben), nicht über den persönlichen Default aus `#85` ermittelt. **Zur Kenntnis:** - Open Food Facts deckt vor allem Lebensmittel ab; bei Drogerie-/Haushaltsartikeln sind Lücken zu erwarten — unproblematisch, da die manuelle Eingabe als Fallback ohnehin vorgesehen ist.
lena commented 2026-08-18 13:13:55 +02:00 (Migrated from git.butzei.de)

design (94_pantry_inventory_phase1_design.md)

Design: #94 — Vorratsschrank (Pantry), Phase 1

Scope-Entscheidung (Architect) — bewusst kleiner als die volle AC-Feature-Parität-Liste

Die Story listet unter "Feature-Umfang" auch Labels, Kommentare, Aktivitäts-Feed und CSV-Import/-Export als
"Ja". Labels/Kommentare/Aktivitäts-Feed sind in diesem Repo aktuell hart an TodoListId/TodoNr gebunden
(LabelEntity, CommentEntity, ActivityEventEntity — keine polymorphe/generische Struktur), genau wie #89
für Shopping Lists strukturelle Kopien statt einer gemeinsamen Tabelle gebaut hat. Das für Pantry ebenfalls
sauber nachzubauen ist ein eigener, non-trivialer Batch Arbeit, der mit der Kernaufgabe dieser Story
(Bestandsführung, atomare Scans, Soll-Mengen, Einkaufslisten-Kopplung — das, was im "As a / I want to / so
that" tatsächlich beschrieben ist) nichts zu tun hat. CSV-Import (anders als Export) braucht ebenfalls einen
komplett neuen Backend-Handler (ImportPantryProductsFromCsvCommand, analog zu #90's
ImportShoppingProductsFromCsvCommand) und ist eine reine Datenportabilitäts-Annehmlichkeit, kein Kernversprechen
der Story. Entscheidung: Labels/Kommentare/Aktivitäts-Feed und CSV-Import/-Export werden in dieser
Runde zurückgestellt und als Folge-Story dokumentiert (#101, siehe unten), statt die Kernfunktion (Barcode-
Scan, atomare Bestandsführung, Soll-Mengen-Kopplung an die Einkaufsliste) durch zusätzlichen Umfang zu
verwässern — gleiches Prinzip wie #89 (WS-Live-Sync → #99) und #90 (Kamera-Scan selbst → #94, jetzt hier
gebaut). Kategorien/Sektionen und manuelle Drag&Drop-Sortierung bleiben im Umfang — beide sind für eine
brauchbare Bestandsübersicht selbst nötig (keine separierbare Zusatzfunktion) und folgen 1:1 dem bereits
bestehenden Shopping-Kategorien-Muster (#80/#81/#90), keine neue Bauweise.

Sharing-Modell: an die Ziel-Einkaufsliste gekoppelt, keine eigene Mitgliederverwaltung

Die Story sagt explizit: "Die Mitgliedschaft läuft über ein eigenes Sharing/Invite analog zu einer Liste, ist
aber an dieselben Freigaben wie die verknüpfte Ziel-Einkaufsliste gekoppelt, statt unabhängig verwaltet zu
werden." Architect-Lesart: keine eigene PantryToUserEntity/Invitation-Struktur — Zugriff auf einen
Vorratsschrank wird direkt über die Mitgliedschaft der verknüpften ShoppingListEntity geprüft
(PantryEntity.TargetShoppingListId → ShoppingListEntity.Users). Wer Mitglied der Ziel-Einkaufsliste ist, hat
automatisch Zugriff auf den Vorratsschrank; wer die Ziel-Einkaufsliste verlässt, verliert ihn automatisch mit.
Das spart eine komplette zweite Sharing-Infrastruktur (kein neues Invite/Member-UI für Pantry) und bildet die
im Text beschriebene Kopplung 1:1 ab, statt sie nur lose zu simulieren.

  • Owner-Aktionen (Vorratsschrank umbenennen/löschen, Ziel-Liste ändern): erfordert Owner-Rolle auf der
    Ziel-Einkaufsliste — analog zu AuthorizeShoppingListOwnerAccessForCurrentUserQuery.
  • Mitglieder-Aktionen (scannen, Artikel anlegen/bearbeiten, Soll-Menge setzen): erfordert nur Mitgliedschaft
    — analog zu AuthorizeShoppingListAccessForCurrentUserQuery.
  • Anlegen eines Vorratsschranks erfordert Mitgliedschaft (nicht Owner) in der gewählten Ziel-Einkaufsliste —
    jedes Haushaltsmitglied mit Zugriff auf die Liste soll einen Vorratsschrank dafür anlegen können.

Datenmodell

  • PantryEntity: Id, Title, TargetShoppingListId (FK ShoppingListEntity). Kein eigenes Users-Feld
    (siehe oben).
  • PantryCategoryEntity: Id, PantryId, Name, Icon?, IsDefault, SortOrder — strukturelle Kopie von
    ShoppingCategoryEntity. Ein Vorratsschrank startet mit einer einzelnen "Allgemein"-Default-Sektion
    (keine 14-Sektionen-CSV wie #90 — dafür gibt es keine passende Quelle; Nutzer legt bei Bedarf eigene an,
    gleiches Muster wie #80's ursprüngliches "General"-Seeding).
  • PantryProductEntity: `Id, PantryId, CategoryId?, Name (PantryProductName), Quantity (PantryQuantity, int

    = 0), TargetQuantity (PantryQuantity?, Soll-Menge), Barcode (ShoppingProductBarcode?, wiederverwendeter Typ —
    dieselbe "eine Produktidentität, kein zweites paralleles Barcode-System" Logik wie #90), SortOrder. PantryProductNameist ein eigener Vogen-Typ (nichtShoppingProductName` wiederverwendet) — Pantry-Artikel
    und Shopping-Produkte sind unterschiedliche Entitäten mit unterschiedlicher Lebensdauer, eine geteilte
    Namens-Value-Object-Identität würde eine Kopplung suggerieren, die nicht besteht (ein Produktname kann in
    beiden Listen unterschiedlich geschrieben sein).

  • Atomare Mengenänderung (Architect-Hinweis der Story): Ein-/Auschecken läuft ausschließlich über
    ExecuteUpdateAsync mit SetProperty(x => x.Quantity, x => x.Quantity + 1) bzw. einem geklemmten Dekrement
    (x.Quantity > 0 ? x.Quantity - 1 : 0, als eine SQL-CASE WHEN-Übersetzung) — kein Read-Modify-Write, exakt
    wie in der Story gefordert. Rückgabewert für den DTO kommt aus einem Folge-SELECT direkt nach dem Update
    (das eigentliche Zähler-Update selbst bleibt atomar; der zurückgegebene DTO-Wert kann bei echter Gleichzeitig-
    keit theoretisch schon den nächsten Scan wiederspiegeln — akzeptabel, das ist erwartetes Verhalten bei
    geteiltem Zugriff, keine Dateninkonsistenz).

"Geteiltes Quellen-Modell" für automatische Einkaufslisten-Einträge — Klärung einer mehrdeutigen AC-Formulierung

Die Story sagt: "Der Eintrag nutzt dasselbe geteilte 'Quelle: manuell/synchronisiert/Vorratsschrank'-
Datenmodell, das #90 für den Online-Warenkorb-Abgleich vorsieht." Wörtlich genommen würde das bedeuten,
ShoppingProductEntity.IsInCart/InCartSource zu setzen, wenn Pantry ein Produkt wegen Soll-Mengen-
Unterschreitung neu auf die Liste schreibt — das ist aber semantisch falsch: ein wegen Mangel neu
hinzugefügtes Produkt ist gerade nicht schon im Warenkorb, das wäre das Gegenteil der eigentlichen
Bedeutung von IsInCart. Architect-Klärung: Der eigentliche Punkt der AC ist "keine zwei parallelen
Quellen-Enums" — das wird erfüllt, indem ShoppingProductInCartSource (der bestehende Typ, bereits mit
Manual/OnlineSynced/PantryAuto) für ein zweites, neues Feld wiederverwendet wird:
ShoppingProductEntity.ActivatedVia (ShoppingProductInCartSource?) — protokolliert, warum ein Produkt aktiv
auf der Liste steht (null = wie bisher manuell, PantryAuto = automatisch wegen Vorratsschrank-Soll-Menge).
Ein Feld für "warum ist es auf der Liste", ein anderes für "ist es schon im Warenkorb" — beide nutzen
denselben Enum-Typ (die eigentliche Wiederverwendung, die die AC verlangt), aber getrennte Spalten mit
getrennter, korrekter Bedeutung. OnlineSynced bleibt für beide Felder reserviert, ungenutzt in dieser Runde.

Soll-Mengen-Workflow

  1. Auschecken unterschreitet Soll-MengeCheckOutPantryProductCommandHandler berechnet nach dem
    atomaren Dekrement fehlend = TargetQuantity - neueMenge (nur wenn TargetQuantity gesetzt und
    neueMenge < TargetQuantity) und schreibt das fehlende Delta auf die TargetShoppingListId-Liste
    (CreateShoppingProductCommand, falls Name noch nicht existiert, dann ActivateShoppingProductCommand
    mit der fehlenden Menge als Text, ActivatedVia = PantryAuto). Läuft im selben Handler wie das Auschecken
    selbst (kein Event-Bus in diesem Projekt üblich für synchrone Folge-Effekte, siehe z. B.
    DeactivateShoppingProductCommandHandler's direkter Aufruf-Stil).
  2. Manuelles Hinzufügen auf die Einkaufsliste, obwohl Pantry-Bestand bereits ausreicht → neue Query
    GetPantryStockForProductNameQuery(shoppingListId, productName), case-insensitiv gegen alle mit dieser
    Liste als Ziel verknüpften Pantries. Frontend ruft sie vor dem Bestätigen der Mengen-Popup auf und zeigt bei
    Treffer den in der AC beschriebenen Hinweistext mit Vorschlag.

Unbekannter Barcode → Open Food Facts

Neuer IOpenFoodFactsClient (typed HttpClient, world.openfoodfacts.org/api/v2/product/{barcode}.json,
Timeout + Fehler-Swallowing → null statt Exception, damit ein Ausfall des externen Diensts nie einen Scan
blockiert, nur auf "manuelle Eingabe nötig" zurückfällt). Barcode wird vor dem Aufruf gegen
^\d{8,14}$ validiert (typische EAN/UPC-Länge) — verhindert, dass beliebiger Nutzer-Text in die externe
Request-URL eingebettet wird (SSRF-/Robustheits-Härtung, siehe Security-Review unten).

Kamera-Barcode-Scan (Frontend)

Neue Abhängigkeit @zxing/browser (reiner JS-Port von ZXing, keine nativen Abhängigkeiten, MIT-Lizenz) für
getUserMedia-Video-Stream-Dekodierung. Neue Komponente PantryBarcodeScanner.tsx: öffnet die Kamera, dekodiert
kontinuierlich, ruft bei Treffer CheckInPantryProductCommand/CheckOutPantryProductCommand (Umschalter
Ein-/Auschecken). Fällt bei fehlender Kamera-Berechtigung oder fehlender Kamera auf ein Texteingabefeld zurück
(gleiches Sicherheitsnetz-Muster wie #90's ShoppingBarcodeScanDialog, die weiterhin nur Text-Eingabe bietet).
Nicht in einem echten Browser mit echter Kamera verifizierbar in dieser Sandbox — wie bereits bei #52/#85
für andere gerätespezifische Funktionen dokumentiert, ist das ein bekanntes, nicht in dieser Runde behebbares
Verifikations-Limit, kein stillschweigend übersprungener Test.

Out of scope / Folge-Punkte (dokumentiert)

  • Labels, Kommentare, Aktivitäts-Feed und CSV-Import/-Export für Pantry-Artikel → #101 (neu angelegt).
  • Phase 2 (Foto-/Sprach-Erkennung) → bereits als #95 vorhanden.
  • Phase 3 (Wandgerät) → kein Software-Scope, Phase 1 liefert aber CheckInPantryProductCommand/
    CheckOutPantryProductCommand bereits als stabile, eigenständig aufrufbare Commands (kein UI-gekoppelter
    Nebeneffekt), sodass ein späteres externes Gerät sie direkt aufrufen kann.
**design** (`94_pantry_inventory_phase1_design.md`) # Design: `#94` — Vorratsschrank (Pantry), Phase 1 ## Scope-Entscheidung (Architect) — bewusst kleiner als die volle AC-Feature-Parität-Liste Die Story listet unter "Feature-Umfang" auch Labels, Kommentare, Aktivitäts-Feed und CSV-Import/-Export als "Ja". Labels/Kommentare/Aktivitäts-Feed sind in diesem Repo aktuell **hart an `TodoListId`/`TodoNr` gebunden** (`LabelEntity`, `CommentEntity`, `ActivityEventEntity` — keine polymorphe/generische Struktur), genau wie `#89` für Shopping Lists strukturelle Kopien statt einer gemeinsamen Tabelle gebaut hat. Das für Pantry ebenfalls sauber nachzubauen ist ein eigener, non-trivialer Batch Arbeit, der mit der Kernaufgabe dieser Story (Bestandsführung, atomare Scans, Soll-Mengen, Einkaufslisten-Kopplung — das, was im "As a / I want to / so that" tatsächlich beschrieben ist) nichts zu tun hat. CSV-Import (anders als Export) braucht ebenfalls einen komplett neuen Backend-Handler (`ImportPantryProductsFromCsvCommand`, analog zu `#90`'s `ImportShoppingProductsFromCsvCommand`) und ist eine reine Datenportabilitäts-Annehmlichkeit, kein Kernversprechen der Story. **Entscheidung:** Labels/Kommentare/Aktivitäts-Feed **und** CSV-Import/-Export werden in dieser Runde zurückgestellt und als Folge-Story dokumentiert (`#101`, siehe unten), statt die Kernfunktion (Barcode- Scan, atomare Bestandsführung, Soll-Mengen-Kopplung an die Einkaufsliste) durch zusätzlichen Umfang zu verwässern — gleiches Prinzip wie `#89` (WS-Live-Sync → `#99`) und `#90` (Kamera-Scan selbst → `#94`, jetzt hier gebaut). Kategorien/Sektionen und manuelle Drag&Drop-Sortierung **bleiben im Umfang** — beide sind für eine brauchbare Bestandsübersicht selbst nötig (keine separierbare Zusatzfunktion) und folgen 1:1 dem bereits bestehenden Shopping-Kategorien-Muster (`#80`/`#81`/`#90`), keine neue Bauweise. ## Sharing-Modell: an die Ziel-Einkaufsliste gekoppelt, keine eigene Mitgliederverwaltung Die Story sagt explizit: "Die Mitgliedschaft läuft über ein eigenes Sharing/Invite analog zu einer Liste, ist aber an dieselben Freigaben wie die verknüpfte Ziel-Einkaufsliste gekoppelt, statt unabhängig verwaltet zu werden." Architect-Lesart: **keine eigene `PantryToUserEntity`/Invitation-Struktur** — Zugriff auf einen Vorratsschrank wird direkt über die Mitgliedschaft der verknüpften `ShoppingListEntity` geprüft (`PantryEntity.TargetShoppingListId → ShoppingListEntity.Users`). Wer Mitglied der Ziel-Einkaufsliste ist, hat automatisch Zugriff auf den Vorratsschrank; wer die Ziel-Einkaufsliste verlässt, verliert ihn automatisch mit. Das spart eine komplette zweite Sharing-Infrastruktur (kein neues Invite/Member-UI für Pantry) und bildet die im Text beschriebene Kopplung 1:1 ab, statt sie nur lose zu simulieren. - **Owner-Aktionen** (Vorratsschrank umbenennen/löschen, Ziel-Liste ändern): erfordert Owner-Rolle auf der Ziel-Einkaufsliste — analog zu `AuthorizeShoppingListOwnerAccessForCurrentUserQuery`. - **Mitglieder-Aktionen** (scannen, Artikel anlegen/bearbeiten, Soll-Menge setzen): erfordert nur Mitgliedschaft — analog zu `AuthorizeShoppingListAccessForCurrentUserQuery`. - **Anlegen eines Vorratsschranks** erfordert Mitgliedschaft (nicht Owner) in der gewählten Ziel-Einkaufsliste — jedes Haushaltsmitglied mit Zugriff auf die Liste soll einen Vorratsschrank dafür anlegen können. ## Datenmodell - **`PantryEntity`**: `Id, Title, TargetShoppingListId (FK ShoppingListEntity)`. Kein eigenes `Users`-Feld (siehe oben). - **`PantryCategoryEntity`**: Id, PantryId, Name, Icon?, IsDefault, SortOrder — strukturelle Kopie von `ShoppingCategoryEntity`. Ein Vorratsschrank startet mit einer einzelnen "Allgemein"-Default-Sektion (keine 14-Sektionen-CSV wie `#90` — dafür gibt es keine passende Quelle; Nutzer legt bei Bedarf eigene an, gleiches Muster wie `#80`'s ursprüngliches "General"-Seeding). - **`PantryProductEntity`**: `Id, PantryId, CategoryId?, Name (PantryProductName), Quantity (PantryQuantity, int >= 0), TargetQuantity (PantryQuantity?, Soll-Menge), Barcode (ShoppingProductBarcode?, wiederverwendeter Typ — dieselbe "eine Produktidentität, kein zweites paralleles Barcode-System" Logik wie `#90`), SortOrder`. `PantryProductName` ist ein eigener Vogen-Typ (nicht `ShoppingProductName` wiederverwendet) — Pantry-Artikel und Shopping-Produkte sind unterschiedliche Entitäten mit unterschiedlicher Lebensdauer, eine geteilte Namens-Value-Object-Identität würde eine Kopplung suggerieren, die nicht besteht (ein Produktname kann in beiden Listen unterschiedlich geschrieben sein). - **Atomare Mengenänderung (Architect-Hinweis der Story):** Ein-/Auschecken läuft ausschließlich über `ExecuteUpdateAsync` mit `SetProperty(x => x.Quantity, x => x.Quantity + 1)` bzw. einem geklemmten Dekrement (`x.Quantity > 0 ? x.Quantity - 1 : 0`, als eine SQL-`CASE WHEN`-Übersetzung) — kein Read-Modify-Write, exakt wie in der Story gefordert. Rückgabewert für den DTO kommt aus einem Folge-`SELECT` direkt nach dem Update (das eigentliche Zähler-Update selbst bleibt atomar; der zurückgegebene DTO-Wert kann bei echter Gleichzeitig- keit theoretisch schon den nächsten Scan wiederspiegeln — akzeptabel, das ist erwartetes Verhalten bei geteiltem Zugriff, keine Dateninkonsistenz). ## "Geteiltes Quellen-Modell" für automatische Einkaufslisten-Einträge — Klärung einer mehrdeutigen AC-Formulierung Die Story sagt: "Der Eintrag nutzt dasselbe geteilte 'Quelle: manuell/synchronisiert/Vorratsschrank'- Datenmodell, das `#90` für den Online-Warenkorb-Abgleich vorsieht." Wörtlich genommen würde das bedeuten, `ShoppingProductEntity.IsInCart`/`InCartSource` zu setzen, wenn Pantry ein Produkt wegen Soll-Mengen- Unterschreitung neu auf die Liste schreibt — das ist aber semantisch falsch: ein wegen Mangel neu hinzugefügtes Produkt ist gerade **nicht** schon im Warenkorb, das wäre das Gegenteil der eigentlichen Bedeutung von `IsInCart`. **Architect-Klärung:** Der eigentliche Punkt der AC ist "keine zwei parallelen Quellen-Enums" — das wird erfüllt, indem `ShoppingProductInCartSource` (der bestehende Typ, bereits mit `Manual/OnlineSynced/PantryAuto`) für ein **zweites, neues Feld** wiederverwendet wird: `ShoppingProductEntity.ActivatedVia (ShoppingProductInCartSource?)` — protokolliert, warum ein Produkt aktiv auf der Liste steht (`null` = wie bisher manuell, `PantryAuto` = automatisch wegen Vorratsschrank-Soll-Menge). Ein Feld für "warum ist es *auf der Liste*", ein anderes für "ist es *schon im Warenkorb*" — beide nutzen denselben Enum-Typ (die eigentliche Wiederverwendung, die die AC verlangt), aber getrennte Spalten mit getrennter, korrekter Bedeutung. `OnlineSynced` bleibt für beide Felder reserviert, ungenutzt in dieser Runde. ## Soll-Mengen-Workflow 1. **Auschecken unterschreitet Soll-Menge** → `CheckOutPantryProductCommandHandler` berechnet nach dem atomaren Dekrement `fehlend = TargetQuantity - neueMenge` (nur wenn `TargetQuantity` gesetzt und `neueMenge < TargetQuantity`) und schreibt das fehlende Delta auf die `TargetShoppingListId`-Liste (`CreateShoppingProductCommand`, falls Name noch nicht existiert, dann `ActivateShoppingProductCommand` mit der fehlenden Menge als Text, `ActivatedVia = PantryAuto`). Läuft im selben Handler wie das Auschecken selbst (kein Event-Bus in diesem Projekt üblich für synchrone Folge-Effekte, siehe z. B. `DeactivateShoppingProductCommandHandler`'s direkter Aufruf-Stil). 2. **Manuelles Hinzufügen auf die Einkaufsliste, obwohl Pantry-Bestand bereits ausreicht** → neue Query `GetPantryStockForProductNameQuery(shoppingListId, productName)`, case-insensitiv gegen alle mit dieser Liste als Ziel verknüpften Pantries. Frontend ruft sie vor dem Bestätigen der Mengen-Popup auf und zeigt bei Treffer den in der AC beschriebenen Hinweistext mit Vorschlag. ## Unbekannter Barcode → Open Food Facts Neuer `IOpenFoodFactsClient` (typed `HttpClient`, `world.openfoodfacts.org/api/v2/product/{barcode}.json`, Timeout + Fehler-Swallowing → `null` statt Exception, damit ein Ausfall des externen Diensts nie einen Scan blockiert, nur auf "manuelle Eingabe nötig" zurückfällt). Barcode wird vor dem Aufruf gegen `^\d{8,14}$` validiert (typische EAN/UPC-Länge) — verhindert, dass beliebiger Nutzer-Text in die externe Request-URL eingebettet wird (SSRF-/Robustheits-Härtung, siehe Security-Review unten). ## Kamera-Barcode-Scan (Frontend) Neue Abhängigkeit `@zxing/browser` (reiner JS-Port von ZXing, keine nativen Abhängigkeiten, MIT-Lizenz) für `getUserMedia`-Video-Stream-Dekodierung. Neue Komponente `PantryBarcodeScanner.tsx`: öffnet die Kamera, dekodiert kontinuierlich, ruft bei Treffer `CheckInPantryProductCommand`/`CheckOutPantryProductCommand` (Umschalter Ein-/Auschecken). Fällt bei fehlender Kamera-Berechtigung oder fehlender Kamera auf ein Texteingabefeld zurück (gleiches Sicherheitsnetz-Muster wie `#90`'s `ShoppingBarcodeScanDialog`, die weiterhin nur Text-Eingabe bietet). **Nicht in einem echten Browser mit echter Kamera verifizierbar in dieser Sandbox** — wie bereits bei `#52`/`#85` für andere gerätespezifische Funktionen dokumentiert, ist das ein bekanntes, nicht in dieser Runde behebbares Verifikations-Limit, kein stillschweigend übersprungener Test. ## Out of scope / Folge-Punkte (dokumentiert) - Labels, Kommentare, Aktivitäts-Feed **und CSV-Import/-Export** für Pantry-Artikel → `#101` (neu angelegt). - Phase 2 (Foto-/Sprach-Erkennung) → bereits als `#95` vorhanden. - Phase 3 (Wandgerät) → kein Software-Scope, Phase 1 liefert aber `CheckInPantryProductCommand`/ `CheckOutPantryProductCommand` bereits als stabile, eigenständig aufrufbare Commands (kein UI-gekoppelter Nebeneffekt), sodass ein späteres externes Gerät sie direkt aufrufen kann.
lena commented 2026-08-18 13:13:55 +02:00 (Migrated from git.butzei.de)

security_prereview (94_pantry_inventory_phase1_security_prereview.md)

Security Pre-Review: #94 — Vorratsschrank (Pantry), Phase 1

Reviewed before implementation, per ai/roles/00_team_overview.md's feature cycle (design → security
pre-review → implementation).

1. Atomare Bestandsänderung (Story's eigener Architect-Hinweis)

Risiko: Read-Modify-Write auf einen geteilten Zähler bei gleichzeitigen Scans mehrerer Haushaltsmitglieder
verliert Updates (klassische Race Condition, in diesem Projekt laut Story bereits mehrfach nachträglich in
Code-Reviews gefunden — #61/#71/#80/#85).
Mitigation: ExecuteUpdateAsync mit SetProperty(x => x.Quantity, x => x.Quantity + 1) (Dekrement mit
geklemmtem CASE WHEN) — eine einzelne atomare SQL-UPDATE-Anweisung, kein vorheriges SELECT des aktuellen
Werts. Verbindlich für Backend-Implementierung, keine Ausnahme.

2. Externe HTTP-Anfrage an Open Food Facts (SSRF-artiges Risiko)

Risiko: Ein vom Client kommender "Barcode"-String fließt direkt in eine vom Server ausgehende URL ein. Ohne
Validierung könnte ein manipulierter Wert (z. B. mit ../ oder einer vollständigen alternativen Host-Angabe,
falls die URL-Konstruktion das zulässt) den Server dazu bringen, gegen ein anderes Ziel als die beabsichtigte
Open-Food-Facts-API zu requesten (interne Netzwerk-Scans, etc.).
Mitigation:

  • Barcode wird vor jedem Open-Food-Facts-Aufruf (nicht im gemeinsam genutzten ShoppingProductBarcode-
    Value-Object selbst, das laut #90 bewusst frei/symbologie-agnostisch bleibt — eine Änderung dort würde
    #90's bestehendes Verhalten überraschend mitändern) gegen ^\d{8,14}$ geprüft, direkt im neuen
    IOpenFoodFactsClient. Ein Wert, der nicht passt, wird nie in die externe URL eingesetzt — die Abfrage
    liefert dann direkt "nicht gefunden" zurück, ohne HTTP-Aufruf.
  • Die Basis-URL (https://world.openfoodfacts.org/...) ist eine feste Konstante im Code, nicht konfigurierbar
    über Nutzereingabe — der Barcode füllt ausschließlich einen Pfad-Segment-Platzhalter, nie das Host/Schema.
  • Timeout (5s) + try/catch um den gesamten Aufruf — ein Ausfall/Hänger des externen Diensts darf niemals den
    eigenen Request blockieren oder eine unbehandelte Exception nach außen durchreichen; fällt bei jedem Fehler
    auf "nicht gefunden, manuelle Eingabe nötig" zurück.
  • Keine Weitergabe von Nutzer-Session-Daten/Auth-Headern an den externen Aufruf (Open Food Facts braucht keine
    Authentifizierung).

3. Sharing-Kopplung an die Ziel-Einkaufsliste (kein eigenes Pantry-Sharing)

Risiko: Wenn die Autorisierungsprüfung nicht sauber über TargetShoppingListId verkettet, könnte ein
Nutzer ohne Zugriff auf die verknüpfte Einkaufsliste trotzdem auf den Vorratsschrank zugreifen (oder
umgekehrt: ein Mitglied verliert beim Verlassen der Liste keinen Pantry-Zugriff, weil die Prüfung gecacht
oder falsch verkettet ist).
Mitigation: Jede Pantry-Autorisierungs-Query (AuthorizePantryAccessForCurrentUserQuery,
AuthorizePantryOwnerAccessForCurrentUserQuery) liest live gegen ShoppingListToUserEntity (kein
Zwischenspeichern der Mitgliedschaft auf der PantryEntity selbst) — identisches Live-Prüf-Muster wie
AuthorizeShoppingListAccessQueryHandler. Ein PantryEntity.TargetShoppingListId-Wechsel (falls je erlaubt)
muss immer sofort die neue Ziel-Liste maßgeblich machen, nie die alte cachen — für Phase 1 wird die Ziel-Liste
nach Anlage nicht änderbar (kein SetPantryTargetListCommand in dieser Story), was dieses Risiko von
vornherein eliminiert statt es zu mitigieren.

4. Löschung der Ziel-Einkaufsliste

Risiko: Löscht der Owner die verknüpfte Einkaufsliste, wird der Vorratsschrank verwaist (Zugriffsprüfung
findet keine ShoppingListEntity mehr, alle Nutzer verlieren Zugriff, Daten bleiben aber unlöschbar in der DB
liegen — ein "unreachable but never deleted"-Datenleck-Muster, das #89's DeleteCurrentUserAccountCommandHandler-
Review bereits einmal für ein ähnliches Szenario gefunden hat).
Mitigation: PantryEntity.TargetShoppingListId bekommt OnDelete(DeleteBehavior.Cascade) — löscht der
Owner die Einkaufsliste, wird der verknüpfte Vorratsschrank (und all seine Artikel/Kategorien) automatisch mit
gelöscht, kein verwaistes Datenrelikt.

5. Konsum externer Antwortdaten (Open Food Facts)

Risiko: Der Produktname aus der externen API fließt in die eigene DB und wird später im Frontend gerendert
— React escaped standardmäßig alle Text-Interpolationen, daher kein direktes XSS-Risiko, aber Längen-/
Plausibilitäts-Validierung fehlt ohne weiteres.
Mitigation: Der von Open Food Facts gelieferte Name durchläuft denselben PantryProductName-Vogen-
Validator (Längenbegrenzung) wie jeder nutzereingegebene Name — keine Sonderbehandlung, kein Vertrauens-
Bonus für externe Daten.

Ergebnis

Keine blockierenden Findings — alle vier identifizierten Risiken sind bereits in die geplante
Implementierung eingearbeitet (siehe Design-Dokument). Freigabe zur Implementierung.

**security_prereview** (`94_pantry_inventory_phase1_security_prereview.md`) # Security Pre-Review: `#94` — Vorratsschrank (Pantry), Phase 1 Reviewed before implementation, per `ai/roles/00_team_overview.md`'s feature cycle (design → security pre-review → implementation). ## 1. Atomare Bestandsänderung (Story's eigener Architect-Hinweis) **Risiko:** Read-Modify-Write auf einen geteilten Zähler bei gleichzeitigen Scans mehrerer Haushaltsmitglieder verliert Updates (klassische Race Condition, in diesem Projekt laut Story bereits mehrfach nachträglich in Code-Reviews gefunden — `#61`/`#71`/`#80`/`#85`). **Mitigation:** `ExecuteUpdateAsync` mit `SetProperty(x => x.Quantity, x => x.Quantity + 1)` (Dekrement mit geklemmtem `CASE WHEN`) — eine einzelne atomare SQL-`UPDATE`-Anweisung, kein vorheriges `SELECT` des aktuellen Werts. **Verbindlich für Backend-Implementierung, keine Ausnahme.** ## 2. Externe HTTP-Anfrage an Open Food Facts (SSRF-artiges Risiko) **Risiko:** Ein vom Client kommender "Barcode"-String fließt direkt in eine vom Server ausgehende URL ein. Ohne Validierung könnte ein manipulierter Wert (z. B. mit `../` oder einer vollständigen alternativen Host-Angabe, falls die URL-Konstruktion das zulässt) den Server dazu bringen, gegen ein anderes Ziel als die beabsichtigte Open-Food-Facts-API zu requesten (interne Netzwerk-Scans, etc.). **Mitigation:** - Barcode wird **vor jedem Open-Food-Facts-Aufruf** (nicht im gemeinsam genutzten `ShoppingProductBarcode`- Value-Object selbst, das laut `#90` bewusst frei/symbologie-agnostisch bleibt — eine Änderung dort würde `#90`'s bestehendes Verhalten überraschend mitändern) gegen `^\d{8,14}$` geprüft, direkt im neuen `IOpenFoodFactsClient`. Ein Wert, der nicht passt, wird nie in die externe URL eingesetzt — die Abfrage liefert dann direkt "nicht gefunden" zurück, ohne HTTP-Aufruf. - Die Basis-URL (`https://world.openfoodfacts.org/...`) ist eine feste Konstante im Code, nicht konfigurierbar über Nutzereingabe — der Barcode füllt ausschließlich einen Pfad-Segment-Platzhalter, nie das Host/Schema. - Timeout (5s) + `try/catch` um den gesamten Aufruf — ein Ausfall/Hänger des externen Diensts darf niemals den eigenen Request blockieren oder eine unbehandelte Exception nach außen durchreichen; fällt bei jedem Fehler auf "nicht gefunden, manuelle Eingabe nötig" zurück. - Keine Weitergabe von Nutzer-Session-Daten/Auth-Headern an den externen Aufruf (Open Food Facts braucht keine Authentifizierung). ## 3. Sharing-Kopplung an die Ziel-Einkaufsliste (kein eigenes Pantry-Sharing) **Risiko:** Wenn die Autorisierungsprüfung nicht sauber über `TargetShoppingListId` verkettet, könnte ein Nutzer ohne Zugriff auf die verknüpfte Einkaufsliste trotzdem auf den Vorratsschrank zugreifen (oder umgekehrt: ein Mitglied verliert beim Verlassen der Liste keinen Pantry-Zugriff, weil die Prüfung gecacht oder falsch verkettet ist). **Mitigation:** Jede Pantry-Autorisierungs-Query (`AuthorizePantryAccessForCurrentUserQuery`, `AuthorizePantryOwnerAccessForCurrentUserQuery`) liest **live** gegen `ShoppingListToUserEntity` (kein Zwischenspeichern der Mitgliedschaft auf der `PantryEntity` selbst) — identisches Live-Prüf-Muster wie `AuthorizeShoppingListAccessQueryHandler`. Ein `PantryEntity.TargetShoppingListId`-Wechsel (falls je erlaubt) muss immer sofort die neue Ziel-Liste maßgeblich machen, nie die alte cachen — für Phase 1 wird die Ziel-Liste nach Anlage **nicht änderbar** (kein `SetPantryTargetListCommand` in dieser Story), was dieses Risiko von vornherein eliminiert statt es zu mitigieren. ## 4. Löschung der Ziel-Einkaufsliste **Risiko:** Löscht der Owner die verknüpfte Einkaufsliste, wird der Vorratsschrank verwaist (Zugriffsprüfung findet keine `ShoppingListEntity` mehr, alle Nutzer verlieren Zugriff, Daten bleiben aber unlöschbar in der DB liegen — ein "unreachable but never deleted"-Datenleck-Muster, das `#89`'s `DeleteCurrentUserAccountCommandHandler`- Review bereits einmal für ein ähnliches Szenario gefunden hat). **Mitigation:** `PantryEntity.TargetShoppingListId` bekommt `OnDelete(DeleteBehavior.Cascade)` — löscht der Owner die Einkaufsliste, wird der verknüpfte Vorratsschrank (und all seine Artikel/Kategorien) automatisch mit gelöscht, kein verwaistes Datenrelikt. ## 5. Konsum externer Antwortdaten (Open Food Facts) **Risiko:** Der Produktname aus der externen API fließt in die eigene DB und wird später im Frontend gerendert — React escaped standardmäßig alle Text-Interpolationen, daher kein direktes XSS-Risiko, aber Längen-/ Plausibilitäts-Validierung fehlt ohne weiteres. **Mitigation:** Der von Open Food Facts gelieferte Name durchläuft denselben `PantryProductName`-Vogen- Validator (Längenbegrenzung) wie jeder nutzereingegebene Name — keine Sonderbehandlung, kein Vertrauens- Bonus für externe Daten. ## Ergebnis Keine blockierenden Findings — alle vier identifizierten Risiken sind bereits in die geplante Implementierung eingearbeitet (siehe Design-Dokument). Freigabe zur Implementierung.
lena commented 2026-08-18 13:13:56 +02:00 (Migrated from git.butzei.de)

security_final (94_pantry_inventory_phase1_security_final.md)

Security Final Review: #94 — Vorratsschrank (Pantry), Phase 1

Reviewed after implementation, against the actually-shipped code (not the plan) — verifying every
finding from the pre-review (94_pantry_inventory_phase1_security_prereview.md) was actually
implemented, plus a fresh pass over the finished diff.

Pre-Review Findings — Verified Against Code

  1. Atomare Bestandsänderung. CheckInPantryProductCommandHandler/CheckOutPantryProductCommandHandler
    both use ExecuteUpdateAsync with in-SQL arithmetic (p.Quantity + 1, geklemmtes p.Quantity > 0 ? p.Quantity - 1 : 0)
    — kein Read-Modify-Write. Verifiziert gegen echtes Postgres in diesem Zyklus (Docker war für diesen
    einen Durchlauf verfügbar) — alle Pantry-Handler-Tests liefen tatsächlich gegen eine reale DB, nicht nur
    gegen den Docker-unabhängigen Compile-Check. Kein LINQ-Übersetzungsfehler auf diesem Pfad.
  2. Open Food Facts SSRF-Härtung. OpenFoodFactsClient.TryLookupProductName validiert den Barcode gegen
    ^\d{8,14}$, bevor er in die Request-URL eingesetzt wird; feste Basis-URL im Code, kein Nutzer-Einfluss
    auf Host/Schema; 5s-Timeout + vollständiges Exception-Swallowing. Bestätigt durch
    OpenFoodFactsClientTests.cs (4 Tests, inkl. "never calls out for a barcode that does not look like a
    real EAN/UPC").
  3. Sharing-Kopplung. AuthorizePantryAccessQueryHandler/AuthorizePantryOwnerAccessQueryHandler lesen
    live gegen ShoppingListToUserEntity (kein Caching auf PantryEntity). Kein
    SetPantryTargetListCommand existiert — die Ziel-Liste ist nach Anlage unveränderlich, wie geplant.
    Bestätigt durch AuthorizePantryAccessQueryHandlerTests/AuthorizePantryOwnerAccessQueryHandlerTests
    (Zugriff gewährt für Mitglied/Owner der Ziel-Liste, verweigert für Fremde).
  4. Löschung der Ziel-Einkaufsliste. PantryEntity.TargetShoppingListId hat OnDelete(DeleteBehavior.Cascade)
    (siehe PantryEntity.cs + generierte Migration) — verifiziert per FK-Konstellation in der Migration.
  5. Externe Antwortdaten. Der von Open Food Facts gelieferte Name durchläuft denselben
    PantryProductName-Validator (zusätzlich in OpenFoodFactsClient selbst auf 100 Zeichen gekappt, bevor
    er überhaupt an PantryProductName.From(...) übergeben wird — doppelte Absicherung, keine Ausnahme für
    externe Daten).

Zusätzliche Findings dieser Runde

  • Kein neuer Finding sicherheitskritisch. Ein während der Verifikation gefundener und behobener Bug
    (ShoppingCategoryResolver's Vogen-.Value.ToLower()-Vergleiche übersetzen sich nicht zu SQL) ist ein
    reiner Korrektheits-/Verfügbarkeits-Bug (bricht die Auto-Kategorisierung), kein Sicherheitsproblem — siehe
    Team-Coach-Memory für die volle technische Einordnung.
  • ScanPantryProductBarcodeCommand's Fallback-Pfad (unbekannter Barcode + manueller Name) legt ein
    neues Produkt in der Default-Kategorie an, ohne erneute Autorisierungsprüfung außerhalb der bereits
    vorhandenen AuthorizePantryAccessForCurrentUserQuery auf Command-Ebene — korrekt, kein zusätzlicher
    Check nötig, da derselbe Nutzer/dieselbe Anfrage.
  • GetPantryStockForProductNameQuery ist auf AuthorizeShoppingListAccessForCurrentUserQuery autorisiert
    (nicht Pantry-spezifisch) — korrekt, da die Anfrage vom Kontext der Einkaufsliste ausgeht (Mengen-Dialog),
    nicht vom Vorratsschrank; ein Nutzer mit Zugriff auf die Einkaufsliste darf wissen, ob eine damit
    verknüpfte Pantry bereits Bestand hat (das ist exakt der Zweck der Kopplung).

Ergebnis

Keine blockierenden Findings. Alle fünf Pre-Review-Punkte sind im finalen Code korrekt umgesetzt und durch
Tests abgedeckt. Freigabe.

**security_final** (`94_pantry_inventory_phase1_security_final.md`) # Security Final Review: `#94` — Vorratsschrank (Pantry), Phase 1 Reviewed after implementation, against the actually-shipped code (not the plan) — verifying every finding from the pre-review (`94_pantry_inventory_phase1_security_prereview.md`) was actually implemented, plus a fresh pass over the finished diff. ## Pre-Review Findings — Verified Against Code 1. **Atomare Bestandsänderung.** `CheckInPantryProductCommandHandler`/`CheckOutPantryProductCommandHandler` both use `ExecuteUpdateAsync` with in-SQL arithmetic (`p.Quantity + 1`, geklemmtes `p.Quantity > 0 ? p.Quantity - 1 : 0`) — kein Read-Modify-Write. **Verifiziert gegen echtes Postgres** in diesem Zyklus (Docker war für diesen einen Durchlauf verfügbar) — alle Pantry-Handler-Tests liefen tatsächlich gegen eine reale DB, nicht nur gegen den Docker-unabhängigen Compile-Check. Kein LINQ-Übersetzungsfehler auf diesem Pfad. 2. **Open Food Facts SSRF-Härtung.** `OpenFoodFactsClient.TryLookupProductName` validiert den Barcode gegen `^\d{8,14}$`, bevor er in die Request-URL eingesetzt wird; feste Basis-URL im Code, kein Nutzer-Einfluss auf Host/Schema; 5s-Timeout + vollständiges Exception-Swallowing. Bestätigt durch `OpenFoodFactsClientTests.cs` (4 Tests, inkl. "never calls out for a barcode that does not look like a real EAN/UPC"). 3. **Sharing-Kopplung.** `AuthorizePantryAccessQueryHandler`/`AuthorizePantryOwnerAccessQueryHandler` lesen live gegen `ShoppingListToUserEntity` (kein Caching auf `PantryEntity`). Kein `SetPantryTargetListCommand` existiert — die Ziel-Liste ist nach Anlage unveränderlich, wie geplant. Bestätigt durch `AuthorizePantryAccessQueryHandlerTests`/`AuthorizePantryOwnerAccessQueryHandlerTests` (Zugriff gewährt für Mitglied/Owner der Ziel-Liste, verweigert für Fremde). 4. **Löschung der Ziel-Einkaufsliste.** `PantryEntity.TargetShoppingListId` hat `OnDelete(DeleteBehavior.Cascade)` (siehe `PantryEntity.cs` + generierte Migration) — verifiziert per FK-Konstellation in der Migration. 5. **Externe Antwortdaten.** Der von Open Food Facts gelieferte Name durchläuft denselben `PantryProductName`-Validator (zusätzlich in `OpenFoodFactsClient` selbst auf 100 Zeichen gekappt, bevor er überhaupt an `PantryProductName.From(...)` übergeben wird — doppelte Absicherung, keine Ausnahme für externe Daten). ## Zusätzliche Findings dieser Runde - **Kein neuer Finding sicherheitskritisch.** Ein während der Verifikation gefundener und behobener Bug (`ShoppingCategoryResolver`'s Vogen-`.Value.ToLower()`-Vergleiche übersetzen sich nicht zu SQL) ist ein reiner Korrektheits-/Verfügbarkeits-Bug (bricht die Auto-Kategorisierung), kein Sicherheitsproblem — siehe Team-Coach-Memory für die volle technische Einordnung. - **`ScanPantryProductBarcodeCommand`'s Fallback-Pfad** (unbekannter Barcode + manueller Name) legt ein neues Produkt in der Default-Kategorie an, ohne erneute Autorisierungsprüfung außerhalb der bereits vorhandenen `AuthorizePantryAccessForCurrentUserQuery` auf Command-Ebene — korrekt, kein zusätzlicher Check nötig, da derselbe Nutzer/dieselbe Anfrage. - **`GetPantryStockForProductNameQuery`** ist auf `AuthorizeShoppingListAccessForCurrentUserQuery` autorisiert (nicht Pantry-spezifisch) — korrekt, da die Anfrage vom Kontext der Einkaufsliste ausgeht (Mengen-Dialog), nicht vom Vorratsschrank; ein Nutzer mit Zugriff auf die Einkaufsliste darf wissen, ob eine damit verknüpfte Pantry bereits Bestand hat (das ist exakt der Zweck der Kopplung). ## Ergebnis Keine blockierenden Findings. Alle fünf Pre-Review-Punkte sind im finalen Code korrekt umgesetzt und durch Tests abgedeckt. Freigabe.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
robert/todo#94
No description provided.