import { requireSession } from "@/server/auth"; import { storage } from "@/server/storage/adapter"; /** * Download-Route für Dateien im Objektspeicher (Garage/S3) anhand ihres Storage-Keys. * * Mandanten-Isolation: * Der Key ist mandantenpräfixiert (`/…`). Er MUSS mit dem Tenant der * aktuellen Session beginnen — ein Fremd-Tenant-Key wird mit 404 abgewiesen * (keine Existenz-Preisgabe). * * TODO(documents): Defense in Depth wiederherstellen — sobald das Craftvia-Document-Modell * existiert, zusätzlich prüfen, dass der Key in einer mandantengebundenen Referenz * (Document.storageKey) vorkommt und der Nutzer das Dokument sehen darf * (document:read bzw. document:read_internal für interne Dokumente). * * Auslieferung mit `Content-Disposition: attachment` und `X-Content-Type-Options: * nosniff` (F-07) — kein Inline-Rendering, kein MIME-Sniffing. * * Route-Handler laufen NICHT durch das Layout-Gate; die Auth wird hier eigenständig * über `requireSession` erzwungen. */ export async function GET( _req: Request, { params }: { params: Promise<{ key: string[] }> }, ) { const session = await requireSession(); const tenantId = session.user.tenantId; const { key: segments } = await params; // Catch-all-Segmente sind bereits URL-dekodiert; zum Objekt-Key zusammenfügen. const key = (segments ?? []).join("/"); // Pfad-Traversal ausschließen und Mandantenpräfix erzwingen. if ( !tenantId || !key || key.includes("..") || key.includes("\0") || !key.startsWith(`${tenantId}/`) ) { return new Response("Nicht gefunden.", { status: 404 }); } const content = await storage.get(key); if (!content) { // Kein Byte-Backend (Stub) oder Objekt fehlt → 404. return new Response("Datei nicht verfügbar.", { status: 404 }); } const filename = content.filename.replace(/["\\]/g, "_"); const headers = new Headers({ "Content-Type": content.contentType ?? "application/octet-stream", "Content-Disposition": `attachment; filename="${filename}"`, "X-Content-Type-Options": "nosniff", "Cache-Control": "private, no-store", }); if (content.size != null) headers.set("Content-Length", String(content.size)); return new Response(content.stream, { headers }); }