import { BadRequestException, Body, Controller, Get, Headers, Param, Post, Put, Req, Res } from '@nestjs/common'; import type { Request, Response } from 'express'; import { MediaService } from './media.service'; import { SessionVerifier } from '../platform/session.verifier'; import { PresignDownloadDto, PresignUploadDto } from './media.dto'; import type { MessagePrincipal } from '../identity/actor.resolver'; /** Types that can execute script if a browser renders them top-level — served as downloads only. */ const SCRIPTABLE_MIMES = new Set(['text/html', 'application/xhtml+xml', 'image/svg+xml', 'text/xml', 'application/xml']); @Controller('v1/media') export class MediaController { constructor( private readonly media: MediaService, private readonly session: SessionVerifier, ) {} /** Authorize an upload → short-lived signed PUT url + object key. */ @Post('presign-upload') async presignUpload(@Body() body: PresignUploadDto, @Headers('authorization') auth?: string) { return this.media.presignUpload(this.principal(auth), { mime: body.mime, sizeBytes: body.sizeBytes }); } /** Authorize a download → short-lived signed GET url. */ @Post('presign-download') async presignDownload(@Body() body: PresignDownloadDto, @Headers('authorization') auth?: string) { return this.media.presignDownload(this.principal(auth), body.contentRef, body.mime); } /** The presigned PUT target — token IS the auth. Reads the raw binary body stream. */ @Put('upload/:token') async upload(@Param('token') token: string, @Req() req: Request) { const data = await readBody(req); if (data.length === 0) throw new BadRequestException('empty upload body'); return this.media.put(token, data); } /** The presigned GET target — token IS the auth. Streams the bytes with their mime. */ @Get('blob/:token') async blob(@Param('token') token: string, @Res() res: Response) { const { data, mime } = await this.media.get(token); res.setHeader('Content-Type', mime); // Never let the browser MIME-sniff an upload into something executable. res.setHeader('X-Content-Type-Options', 'nosniff'); // Script-capable types must not render inline from our origin (stored-XSS) — force a download. // Images/video/audio/pdf stay inline so the app can preview them. Note /