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 { let benutzer: { id: string; rolle: Parameters[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; } }