refactor(quick-260922-hk4): Bilderrahmen-Bilder in user-files statt in der Datenbank

- Bytes liegen unter user-files/dashboard-images/<userId>/<id>.<ext>, die
  Zeile haelt nur noch storagePath (Muster User.avatarPath)
- Dateiname immer servergeneriert: UUID der Zeile + Endung aus dem
  ERKANNTEN Mime-Typ, originalName kommt in keinem Pfad vor (T-HK4-01)
- Migration 20260922120000: storagePath dazu, data wird NULLbar, kein DROP
  (zweistufig, T-HK4-03); system_read_policy fuer den Umzug
- onApplicationBootstrap zieht Altbestand automatisch um: systemgebunden
  lesen, je Zeile mandantengebunden schreiben (Muster DKV-Planer)
- Upload nimmt die Zeile bei fehlgeschlagenem Schreiben zurueck, Loeschen
  entfernt die Datei mit, fehlende Datei -> 404 (T-HK4-04)
- 11 neue Dienst-Tests gegen ein echtes Temp-Verzeichnis (kein fs-Mock)
- Zugriffsklassifikation: Stand system-gebunden, Zahlen nachgemessen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 15:22:47 +02:00
parent 441854af72
commit 9039cea686
6 changed files with 658 additions and 79 deletions
@@ -150,8 +150,24 @@ const RELATION_SPEC_EXCEPTIONS = new Set<string>(['apps/api/src/tenders/backfill
* der Schleife ist `tenant.findMany` auf `Tenant`, das in keiner Migration
* eine Regel traegt — kein Systemkontext noetig, Datei unveraendert.
* Summe: 4 Dateien, 5 Aufrufe.
*
* SIEBTER FALL (quick-260922-hk4): `dashboard-images.service.ts`, EIN
* Aufruf, ausschliesslich in `onApplicationBootstrap()` — der einmalige
* Umzug der Bilderrahmen-Bilder aus der Spalte `data` in den Dateibereich.
* Ein Startpfad hat keinen Mandanten im Ruecken und muss die noch nicht
* umgezogenen Zeilen ALLER Mandanten sehen; die passende Regel
* `system_read_policy ... FOR SELECT` auf "DashboardImage" legt die
* Migration 20260922120000 an. Dieselbe Datei bedient daneben Anfragewege
* (`list`/`upload`/`getBytes`/`remove`) — die bleiben ausnahmslos
* mandantengebunden, und auch der Umzug SCHREIBT je Zeile ueber
* `forTenant(prisma, row.tenantId, row.userId)`, nie ueber den
* Systemklienten. Praezedenz fuer "ein Dienst mit Anfrageweg UND
* systemgebundenem Startpfad": `ldap-config.service.ts`, dessen
* Nachverschluesselung in `onApplicationBootstrap()` genauso gebaut ist.
* Summe neu: 5 Dateien, 6 Aufrufe.
*/
const FORSYSTEM_ALLOWED_CALL_SITES = new Map<string, number>([
['apps/api/src/dashboard/dashboard-images.service.ts', 1],
['apps/api/src/dkv/dkv.service.ts', 1],
['apps/api/src/ldap/ldap-config.service.ts', 2],
['apps/api/src/tenders/tender-digest.scheduler.ts', 1],