import { BadRequestException, Controller, Get, Inject, Param, Query, Res, StreamableFile, } from '@nestjs/common'; import type { DesktopLatestResponse, DesktopUpdateResponse } from '@tessera/shared'; import type { Response } from 'express'; import { Public } from '../auth/decorators/public.decorator'; import { DesktopService, safeOrigin } from './desktop.service'; @Controller('desktop') export class DesktopController { // `@Inject()` explizit (nicht nur der Konstruktor-Typ): Vitest transpiliert // ueber esbuild, das `emitDecoratorMetadata` nicht respektiert -- ohne den // expliziten Token findet Nests DI in diesem einen HTTP-Durchstich-Test // (desktop.service.spec.ts) keinen Provider und `desktopService` bleibt // `undefined`. Im echten Build (tsc via `nest build`) waere das auch ohne // `@Inject()` korrekt aufgeloest worden. constructor(@Inject(DesktopService) private readonly desktopService: DesktopService) {} // Bewusst oeffentlich (D-10, gleicher Grund wie HealthController.getVersion, // T-KU1-03): die Anmeldeseite zeigt den Download-Link, bevor eine Anmeldung // existiert. @Public() @Get('latest') getLatest(): DesktopLatestResponse { return this.desktopService.getLatest(); } // Bewusst oeffentlich (D-10, wie latest/download): der Desktop-Client prueft // beim Start vor jeder Anmeldung. Statische Route VOR `download/:platform` // (Konvention Route-Order, auch wenn sich die beiden hier nicht // ueberschatten). 204 ist der vom Updater-Plugin definierte Status fuer // "kein Update"; `passthrough` + `res.status(204)` funktioniert, weil Nest // den Standardstatus VOR dem Handler setzt und die Antwort danach ohne // eigenen Statuscode schreibt -- der Handler-Status gewinnt. `current` // wird nicht deklariert, weil der Service es nicht auswertet. @Public() @Get('update') update( @Query('target') target: unknown, @Query('arch') arch: unknown, @Query('base') base: unknown, @Res({ passthrough: true }) res: Response, ): DesktopUpdateResponse | undefined { const origin = safeOrigin(base); if (!origin) { throw new BadRequestException( 'base must be an http(s) origin without path, query or credentials', ); } const result = this.desktopService.getUpdate({ target, arch, origin }); if (!result) { res.status(204); return undefined; } return result; } // Bewusst oeffentlich (D-10): der Download selbst braucht keine Anmeldung, // gleicher Grund wie getLatest oben. @Public() @Get('download/:platform') download(@Param('platform') platform: string): StreamableFile { const { stream, entry } = this.desktopService.getPackage(platform); return new StreamableFile(stream, { type: 'application/octet-stream', disposition: `attachment; filename="${entry.name}"`, length: entry.size, }); } }