feat(260923-lrr): API — Favoriten-Symbol hochladen, Vorrang, Versionszaehler, Abrufprobe

- FavoriteLink: neue Spalten uploadedIconMime/iconVersion (Migration 20260923160000)
- favorite-icon-files.ts: Erkennung PNG/JPEG/GIF/WebP/ICO/SVG, Pfadbildung ohne
  Byte aus der Anfrage im Pfad (T-LRR-01), best-effort Dateientfernung
- FavoritesService: uploadIcon/removeUploadedIcon, Vorrang der hochgeladenen
  Datei in getIconBytes, Abrufprobe fuer eine neue iconUrl (422 statt stiller
  Speicherung), iconVersion-Erhoehung bei jeder Aenderung der Symbolquelle
- FavoritesController: POST/DELETE /favorites/:id/icon, Cache-Control private
- T-LRR-07 (Restrisiko aus dem Plan-Threat-Model geschlossen, ueber den Plan
  hinaus): DashboardService.removeWidget/deleteDashboard raeumen jetzt die
  Symboldateien der per Datenbank-Kaskade mitgeloeschten Favoriten auf
  (best effort, nie blockierend)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 16:04:29 +02:00
parent bf4384ad73
commit 7704372c3c
11 changed files with 1454 additions and 21 deletions
@@ -0,0 +1,37 @@
-- quick-260923-lrr — Favoriten: eigenes Symbol hochladen, Zwischenspeicher
-- nach Aenderung erneuern.
--
-- Zwei neue Spalten auf "FavoriteLink":
--
-- "uploadedIconMime" TEXT NULL — der an den Bytes ERKANNTE Typ eines
-- hochgeladenen eigenen Symbols (PNG/JPEG/GIF/WebP/ICO/SVG); NULL, wenn
-- kein eigenes Symbol hochgeladen wurde. Es wird KEIN Pfad gespeichert:
-- die Datei liegt vollstaendig ableitbar unter
-- "user-files/favorite-icons/<userId>/<id>.<ext>" (Endung aus dem
-- erkannten Typ) — damit gelangt auch bei einer vollstaendigen
-- Zeilenauslieferung (Prisma liefert die ganze Zeile an den Client) kein
-- Serverpfad in eine API-Antwort. Vorrang vor "iconUrl": ist der Wert
-- gesetzt, liefert GET /favorites/:id/icon die hochgeladene Datei statt
-- die gespeicherte Logo-Adresse abzurufen.
--
-- "iconVersion" INTEGER NOT NULL DEFAULT 0 — Zaehler fuer die ausgelieferte
-- Symbol-Adresse (?v=<iconVersion>). Steigt genau dann (Prisma
-- { increment: 1 }), wenn sich die angezeigte Symbolquelle aendert: eine
-- gespeicherte "iconUrl" weicht vom alten Wert ab, ein Symbol wird
-- hochgeladen, oder ein hochgeladenes Symbol wird entfernt. NICHT bei
-- Titel-, Link-Adress- oder Reihenfolgeaenderung ohne Symbolwechsel.
-- Bestandszeilen starten bei 0 — das unterscheidet sich von der vorher
-- unversionierten Adresse, ein bereits 24 Stunden im Browser
-- zwischengespeichertes Symbol wird dadurch beim naechsten Laden sofort
-- ungueltig.
--
-- Die bestehende RLS-Regel "tenant_isolation_policy" auf "FavoriteLink"
-- (Migration 20260911120000, zeilenbezogen ueber "tenantId"/"userId")
-- braucht fuer zwei zusaetzliche Spalten KEINE Anpassung — sie schuetzt
-- Zeilen, nicht Spalten. Kein CREATE/DROP POLICY in dieser Migration.
--
-- "migrate deploy" wendet diese Migration beim Start an
-- (apps/api/scripts/migrate-and-start.sh) — kein manueller Schritt.
ALTER TABLE "FavoriteLink" ADD COLUMN "uploadedIconMime" TEXT;
ALTER TABLE "FavoriteLink" ADD COLUMN "iconVersion" INTEGER NOT NULL DEFAULT 0;
+10
View File
@@ -419,6 +419,16 @@ model FavoriteLink {
title String
url String
iconUrl String?
// quick-260923-lrr: Typ eines hochgeladenen eigenen Symbols (erkannt an
// den Bytes, NIE aus der Anfrage uebernommen); null = kein hochgeladenes
// Symbol. Kein Pfad gespeichert — die Datei liegt ableitbar unter
// user-files/favorite-icons/<userId>/<id>.<ext>.
uploadedIconMime String?
// quick-260923-lrr: Zaehler fuer die ausgelieferte Symbol-Adresse
// (?v=<iconVersion>), steigt bei jeder Aenderung der Symbolquelle
// (neue iconUrl, Upload, Entfernen) — macht den 24-h-Browser-Zwischenspeicher
// nach einer Aenderung sofort ungueltig.
iconVersion Int @default(0)
position Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt