62 lines
2.5 KiB
TypeScript
62 lines
2.5 KiB
TypeScript
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;
|
||
}
|
||
}
|