- ISMS-Routen, Actions, Server-/Lib-Code, Komponenten, Prisma-Modelle, Seeds, Importer, Skripte und ISMS-Tests entfernt (Fundament bleibt: Auth, Identity, MFA/WebAuthn, RBAC, Audit, Mail, Storage, Backup/DSGVO, Plattform-Admin) - Schema auf Fundament-Modelle reduziert; TenantSettings generisch (+phone/email) - TENANT_MODELS (db.ts, backup/topology.ts) und PII-Felder ausgedünnt - RBAC: Rollen tenant-admin/backoffice/team-lead/technician + Craftvia-Permissions - Modul-Katalog (customers, sites, teams, work_orders, imports, field, reports, emergency, documents, notifications, lotse) + Navigation aus src/lib/nav.ts - Modul-Routen mit requireModule-Layout und Platzhalterseite - Message-Katalog je Namespace (messages/<locale>/<namespace>.json), fs-Loader - check-module-guards: Modul-Key aus src/server/actions/<moduleKey>/ - Provisionierung, Admin-Konsole, Einstellungen, Files-Route, Mail entkoppelt - Seed minimal (demo/demo2, Nutzer je Rolle); Fundament-Tests auf Role/ NotificationPreference-Fixtures umgestellt Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
62 lines
2.2 KiB
TypeScript
62 lines
2.2 KiB
TypeScript
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 (`<tenantId>/…`). 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 });
|
|
}
|