import { Controller, Delete, Get, Param, Post, Res, UploadedFile, UseInterceptors, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { Response } from 'express'; import { CurrentUser } from '../auth/decorators/current-user.decorator'; import type { AuthUser, UploadedFileLike } from '../auth/types/auth-user'; import { DASHBOARD_IMAGE_MAX_BYTES } from './dashboard-image-rules'; import { DashboardImagesService } from './dashboard-images.service'; /** * Bilder des Bilderrahmen-Widgets (quick-260921-pi9). * * Offen fuer ALLE angemeldeten Rollen: bewusst KEIN Rollen-Dekorator * (Muster bug-reports.controller.ts — der globale `JwtAuthGuard` verlangt * weiterhin eine Sitzung). Mandant und Benutzer kommen NUR aus dem * Sitzungsnachweis (`@CurrentUser()`), nie aus dem Rumpf (T-PI9-11); der * Multipart-Rumpf hat genau das Feld `image`. * * Groessenlimit JE ROUTE (T-PI9-02): `FileInterceptor` nimmt genau eine * Datei bis 5 MiB; multers `LIMIT_FILE_SIZE` bildet Nest auf 413 ab (die * deutsche Meldung dafuer entsteht im Web-Klienten aus dem Status). * `main.ts` bleibt ohne globales Body-Limit. * * Routen (statische VOR `:id`, sonst 404-Shadowing): * - GET /dashboard/images — eigene Bilder, nur Metadaten * - POST /dashboard/images — Bild hochladen (multipart-Feld `image`) * - GET /dashboard/images/:id — Bytes eines eigenen Bildes * - DELETE /dashboard/images/:id — eigenes Bild loeschen */ @Controller('dashboard/images') export class DashboardImagesController { constructor(private readonly service: DashboardImagesService) {} @Get() async list(@CurrentUser() user: AuthUser) { return this.service.list(user.id, user.tenantId); } @Post() @UseInterceptors( FileInterceptor('image', { limits: { fileSize: DASHBOARD_IMAGE_MAX_BYTES, files: 1 } }), ) async upload(@CurrentUser() user: AuthUser, @UploadedFile() file?: UploadedFileLike) { return this.service.upload(user, file); } /** * Binaerantwort mit den Headern aus T-PI9-06/08/10: der Typ kommt aus der * gespeicherten Magic-Byte-Erkennung, `nosniff` verbietet dem Browser das * Umdeuten, `Content-Disposition: inline` traegt bewusst KEINEN Dateinamen * (`originalName` gehoert in keinen Header), `private` haelt gemeinsame * Zwischenspeicher (Nginx Proxy Manager) fern, die CSP laesst ein direkt im * Tab geoeffnetes Bild ohne Skript und ohne Tessera-Origin laufen (Muster * favorites.controller.ts `getIcon`). * * Prisma 6 liefert `Bytes` als `Uint8Array` — `Buffer.from(...)` ueber * denselben Speicher, keine Kopie, keine Zusicherung. */ @Get(':id') async getImage( @Param('id') id: string, @CurrentUser() user: AuthUser, @Res() res: Response, ) { const { mimeType, data } = await this.service.getBytes(id, user.id, user.tenantId); res.setHeader('Content-Type', mimeType); res.setHeader('Cache-Control', 'private, max-age=86400'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('Content-Disposition', 'inline'); res.setHeader('Content-Security-Policy', "default-src 'none'; sandbox"); res.send(Buffer.from(data.buffer, data.byteOffset, data.byteLength)); } @Delete(':id') async remove(@Param('id') id: string, @CurrentUser() user: AuthUser) { return this.service.remove(id, user.id, user.tenantId); } }