de81c74e53
- Neuer oeffentlicher Endpunkt (vor download/:platform): base-Origin wird
per safeOrigin validiert (nur http/https, kein Pfad/Query/Fragment/
Userinfo, sonst 400) und nur zum Bau der absoluten Download-URL genutzt,
nie serverseitig abgerufen
- 204 ohne Body bei fremder Plattform/Architektur, fehlendem Manifest,
fehlender Signatur oder fehlendem/ungueltigem updateVersion; sonst
{ version, pub_date (nur RFC 3339), url, signature, notes }
- Manifest-Felder signature (je Plattform) und updateVersion optional in
@tessera/shared, Eintragspruefung akzeptiert nur String-Signaturen
- 10 neue Spec-Tests, Test 11 erweitert (23 gesamt); latest/download
unveraendert
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
78 lines
2.8 KiB
TypeScript
78 lines
2.8 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
}
|