- 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>
(req as any) und @Req() req: any durch AuthenticatedRequest ersetzt in
dashboard, favorites, calendar, groups, module-grants, module-registry,
tenders, dkv, ldap, settings; @CurrentUser() in user.controller auf AuthUser.
Die abwehrenden Pruefungen ("No tenant context", "No user context") bleiben
lebendig, weil user auf dem Anfragetyp wahlfrei ist - genau das beschreibt
den Zustand auf oeffentlichen Wegen.
Nebengewinn ohne neue Zusicherungen: req.tenantId as string | undefined
(dkv, settings), file.buffer as Buffer und file.mimetype as string
(dkv, user) sind weggefallen, weil der Typ sie jetzt traegt.
BEFUND 1 (D-03, gemeldet) dashboard.controller.ts:74 alt: der Handler las
req.user?.role NACH extractContext und gab sie an getWidgets(role: Role)
weiter, das eine Rolle zwingend verlangt. Die Annahme "hier gibt es immer
einen Aufrufer" stimmt - die Pruefung "No user context" erzwingt sie -, aber
sie stand in einer anderen Methode, wo der Compiler sie nicht sehen konnte.
extractContext gibt die Rolle jetzt mit zurueck: keine neue Pruefung, kein
erfundener Wert, gleiche Reihenfolge, gleiche Meldungen.
BEFUND 2 (D-03, gemeldet) tenders.controller.ts:142: resolveRequestingTenantId
erklaerte string | undefined, liest aber req.tenantId, das TenantGuard fuer
einen SUPER_ADMIN ohne Mandanten auf null setzt. Die Erklaerung war also nie
vollstaendig. Erweitert auf string | null | undefined, und buildTenderWhere
nimmt string | null - beides nur Erklaerung, kein Verhalten: die Funktion
entscheidet seit jeher ueber Wahrheitswert und faellt bei beiden zu
(nur global sichtbare Ausschreibungen).
Fixtures in user.controller.spec.ts ergaenzt (username, mustChangePassword,
originalname, size). Testzahlen unveraendert.
noExplicitAny in apps/api/src: 137 -> 66. type-check 4/4, lint 5/5,
apps/api 72/1143, apps/web 73/531, tenant.guard.ts unveraendert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
- icon-discovery.service.ts: undicis eigenes fetch mit Modul-Singleton
LENIENT_TLS_AGENT (Agent({ connect: { rejectUnauthorized: false } }))
als dispatcher in fetchWithRedirectGuard, der einzigen Ausgangsstelle
fuer HTML-Ermittlung und Icon-Byte-Holen; SSRF-Schutz unveraendert
- undici 7.28.0 (bereits im Lockfile aufgeloest) als direkte Abhaengigkeit
von @tessera/api via pnpm add --offline
- PUT /favorites/order (ReorderFavoritesDto) vor den :id-Routen;
FavoritesService.reorder() setzt position=index fuer die Favoriten
eines Widgets in EINER withTenantTransaction, userId+widgetId in jeder
Bedingung (zweites Netz), eine BadRequestException fuer alle
Abweichungen (T-JDD-06)
- getIcon: X-Content-Type-Options nosniff + restriktive CSP (T-JDD-02)
- 10 neue Tests (3 Dispatcher, 7 reorder); volle API-Suite 68 Dateien/
1101 Tests und type-check gruen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- favorites.service.ts: alle fuenf Methoden nehmen tenantId als ersten
Parameter, laufen je ueber EINEN Klienten tenantPrisma (7 gebundene
Favoritenzugriffe, 5 Aufrufstellen); create() prueft vor der Icon-Suche,
dass das Ziel-Widget dem Aufrufer gehoert (T-GWH-05, Befund F aus Aufgabe 1
bestaetigt den Fremdschluessel-Durchgriff) -- Widget not found fuer alle
drei Faelle (existiert nicht/Kollege/fremder Mandant)
- favorites.controller.ts: reicht tenantId an alle fuenf Aufrufe durch,
extractContext unveraendert (dashboard-Praezedenzfall)
- settings.service.ts: getSmtpConfig/saveSmtpConfig/getDecryptedSmtpConfig
je EIN Klient (3 gebundene Zugriffe, 3 Aufrufstellen) -- Befund K damit
erfuellt; Startpfad umbenannt in loadAnySmtpConfigForStartupTransport(),
bleibt bewusst ungebunden (sechster Fall der Hintergrunddienst-Falle,
WINDOWS #TBD-GWH -- Aufgabe 3 vergibt die Nummer)
- mail.module.ts: ruft den umbenannten Startpfad auf, Kommentar nennt beide
Zustaende statt "single-tenant default"
- Vier Falsifizierungsnachweise durchgefuehrt und zurueckgenommen (siehe
SUMMARY): (a) 3 Faelle rot, (b) 4 Faelle rot, (c) 9 Faelle rot, (d) 4 Faelle
rot
Bekannt und erwartet (siehe SUMMARY, Praezedenzfall 260911-fh9): zwischen
dieser Aufgabe und Aufgabe 3 ist rls-access-inventory.spec.ts rot (2
Faelle) -- die Klassifikationstabelle ist noch nicht nachgezogen, das ist
Aufgabe 3.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
Ownership-scoped (userId, matching update/remove) icon byte proxy.
Loads the row's stored iconUrl server-side and streams it through
IconDiscoveryService.fetchIconBytes -- never accepts a client-supplied
URL, so this can't become an open SSRF proxy.
Not-found/not-owned/no-icon -> 404. Upstream fetch failure (unreachable,
timeout, non-image, SSRF-blocked) -> 502, never a 200 with a placeholder.
Success sets Cache-Control so the browser doesn't refetch every load.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- CR-01: fix SSRF bypass — isPrivateIpv6 now delegates ::ffff:<ipv4> to
isPrivateIpv4, covering 172.16-31.x and 169.254.x ranges
- CR-02: add ParseUUIDPipe to GET /favorites widgetId param + service guard
so missing widgetId returns 400 instead of leaking all user favorites
- WR-01: link-widget — replace raw 'link.error' key with t('link.error') (4 sites)
- WR-02: favorites-widget — fix load-path error to use t('favorites.error')
- WR-03: widget-catalog-modal — move aria-hidden from outer wrapper to backdrop
- WR-04: calculator — remove duplicate M button (MR clone); MC/MR/M+/M−/MS remain
- WR-05: schema — add FavoriteLink→WidgetInstance FK with onDelete:Cascade
- IN-01: create-widget.dto.ts — update comment from four to eight supported types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>