feat(api): Favoriten — Symbol trotz Zertifikatsfehler holen, Reihenfolge per PUT /favorites/order speichern

- 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>
This commit is contained in:
2026-09-17 14:36:03 +02:00
parent e7633e15de
commit 2a562d0b14
8 changed files with 373 additions and 11 deletions
@@ -8,12 +8,14 @@ import {
ParseUUIDPipe,
Patch,
Post,
Put,
Query,
Req,
Res,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { CreateFavoriteDto } from './dto/create-favorite.dto';
import { ReorderFavoritesDto } from './dto/reorder-favorites.dto';
import { UpdateFavoriteDto } from './dto/update-favorite.dto';
import { FavoritesService } from './favorites.service';
@@ -35,6 +37,8 @@ import { FavoritesService } from './favorites.service';
* Routes:
* - GET /favorites?widgetId= — list favorites for a widget instance
* - POST /favorites — create a favorite (triggers server-side icon discovery)
* - PUT /favorites/order — reorder favorites for a widget instance (260917-jdd)
* - GET /favorites/:id/icon — stream a favorite's stored icon bytes
* - PATCH /favorites/:id — update a favorite (ownership verified in service)
* - DELETE /favorites/:id — delete a favorite (ownership verified in service)
*/
@@ -77,6 +81,23 @@ export class FavoritesController {
return this.favoritesService.create(tenantId, userId, dto);
}
/**
* PUT /favorites/order — persists the display order for a widget's
* favorites (260917-jdd). Declared BEFORE the `:id` routes below on
* purpose (NestJS route order — a later `:id` route would otherwise
* shadow the literal segment "order"; precedent tenders.controller.ts
* Z. 636-648).
*/
@Put('order')
async reorder(
@Body() dto: ReorderFavoritesDto,
@Req() req: Request,
) {
const { userId, tenantId } = this.extractContext(req);
return this.favoritesService.reorder(tenantId, userId, dto);
}
/**
* GET /favorites/:id/icon — streams the stored icon bytes for a favorite
* owned by the caller, from Tessera's own origin. This avoids the browser
@@ -101,6 +122,12 @@ export class FavoritesController {
res.setHeader('Content-Type', contentType);
res.setHeader('Cache-Control', 'public, max-age=86400');
// 260917-jdd: die Bytes kommen jetzt auch von Hosts ohne gueltiges
// Zertifikat. Als <img>-Unterressource ignoriert der Browser diese
// Header, aber ein direkt im Tab geoeffnetes SVG laeuft damit ohne
// Skript und ohne Tessera-Origin (T-JDD-02).
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Content-Security-Policy', "default-src 'none'; sandbox");
res.send(body);
}