Files
Laufzettel/src/app/api/anhaenge/[id]/route.ts
T
florian.wilde 3ccf3aa2d9
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(anhaenge): Fotos und Dateien an einem Schritt (#52) (#59)
2026-10-05 20:49:26 +00:00

62 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { AnhangFehler, anhangLesen } from "@/server/services/anhaenge";
import { MandantFehler } from "@/server/mandant";
import { RechteFehler, rechtErforderlich } from "@/server/auth/sitzungspflicht";
import { istBild } from "@/lib/anhaenge";
/**
* Einen Anhang ausliefern (#032).
*
* **Route Handler prüfen selbst.** Die Hülle unter `src/app/(app)/layout.tsx`
* deckt sie nicht ab, und der Proxy sieht nur, **ob** ein Cookie da ist, nie was
* drinsteht (CLAUDE.md). Hier zählt das doppelt: eine Adresse, die eine Datei
* ohne Prüfung ausliefert, umgeht alles andere.
*
* `Content-Disposition: inline` nur für Bilder. Ein PDF wird **heruntergeladen**
* statt im Browser gerendert: ein PDF kann Skripte enthalten, und ein Anhang,
* den jemand aus einem fremden Lauf öffnet, soll nichts ausführen.
*/
export async function GET(
_anfrage: Request,
{ params }: { params: Promise<{ id: string }> },
): Promise<Response> {
let benutzer: { id: string; rolle: Parameters<typeof anhangLesen>[1]["rolle"] };
try {
const zugang = await rechtErforderlich("lesen");
benutzer = { id: zugang.benutzer.id, rolle: zugang.benutzer.rolle };
} catch (fehler) {
if (fehler instanceof RechteFehler) {
return Response.json({ fehler: "Nicht angemeldet." }, { status: 401 });
}
throw fehler;
}
const { id } = await params;
try {
const datei = await anhangLesen(id, benutzer);
// Der Dateiname steckt in einer Kopfzeile. `dateinameSaeubern` hat
// Steuerzeichen und Anführungszeichen schon beim Anlegen entfernt.
const anzeige = istBild(datei.medientyp) ? "inline" : "attachment";
return new Response(new Uint8Array(datei.inhalt), {
headers: {
"Content-Type": datei.medientyp,
"Content-Length": String(datei.groesse),
"Content-Disposition": `${anzeige}; filename="${datei.dateiname}"`,
// Privat: ein Anhang gehört einem Kunden, kein Zwischenspeicher darf ihn
// an den nächsten Betrachter weitergeben.
"Cache-Control": "private, max-age=0, must-revalidate",
"X-Content-Type-Options": "nosniff",
},
});
} catch (fehler) {
if (fehler instanceof MandantFehler) {
// Nicht vorhanden und nicht sichtbar sind dieselbe Antwort – die
// Unterscheidung wäre schon eine Auskunft über fremde Kunden.
return Response.json({ fehler: "Diesen Anhang gibt es nicht." }, { status: 404 });
}
if (fehler instanceof AnhangFehler) {
return Response.json({ fehler: fehler.message }, { status: 404 });
}
throw fehler;
}
}