- db.ts: tenantTransaction() – atomar auch bei RLS_ENFORCED=true (AsyncLocalStorage bindet Operationen an eine craftvia_app-Transaktion, Kontext einmal gesetzt, verschachtelte Aufrufe treten bei, fremder Mandant wird abgewiesen) - services/context.ts: inTransaction(ctx, fn); imports/confirm.ts umgestellt - next.config.ts: EMBEDDABLE_FILE_ROUTES mit frame-ancestors 'self'/SAMEORIGIN (PDF-Vorschau Prüfmaske), proxyClientMaxBodySize 26mb (Import bis 25 MB) - test-rls-enforcement: RLS-URL-Default aus DATABASE_URL (Lane-DBs) - dsgvo/pii-fields: 26 Personenreferenzen des Craftvia-Domänenmodells - ARCHITEKTUR §4.8: Transaktions-, Header-, Upload-, Versions- und PII-Regeln - Test test-tenant-transaction (Commit/Rollback/Fremdmandant/Verschachtelung), grün im Owner- und im RLS-Modus Gate: tsc, lint, build, 31/31 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
84 lines
3.5 KiB
TypeScript
84 lines
3.5 KiB
TypeScript
import type { NextConfig } from "next";
|
|
import createNextIntlPlugin from "next-intl/plugin";
|
|
|
|
// Im Dev-Betrieb braucht Turbopack/HMR 'unsafe-eval' und WebSocket-Verbindungen
|
|
// zum Dev-Server. Diese Lockerungen gelten NUR in der Entwicklung, nie im Build.
|
|
const isDev = process.env.NODE_ENV !== "production";
|
|
|
|
// Content-Security-Policy (F-07).
|
|
//
|
|
// Hinweis Skripte: 'unsafe-inline' ist ein bewusster Zwischenstand. Next.js liefert
|
|
// seinen Hydration-Bootstrap als Inline-Skript aus; eine Nonce-basierte CSP (Nonce in
|
|
// proxy.ts erzeugen und durchreichen) ist ein eigenes Folgepaket.
|
|
const csp = [
|
|
"default-src 'self'",
|
|
`script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ""}`,
|
|
// Next.js und Tailwind v4 setzen Inline-Styles.
|
|
"style-src 'self' 'unsafe-inline'",
|
|
// data: für den MFA-QR-Code; blob: für clientseitige Foto-Vorschauen (Einsatz-Fotos).
|
|
"img-src 'self' data: blob:",
|
|
// blob: für lokale Wiedergabe von Sprachnotizen vor dem Upload.
|
|
"media-src 'self' blob:",
|
|
"font-src 'self' data:",
|
|
// Im Dev zusätzlich der HMR-WebSocket des Dev-Servers.
|
|
`connect-src 'self'${isDev ? " ws: wss:" : ""}`,
|
|
// PWA: Service Worker nur vom eigenen Ursprung.
|
|
"worker-src 'self'",
|
|
"manifest-src 'self'",
|
|
"frame-ancestors 'none'",
|
|
"form-action 'self'",
|
|
"base-uri 'self'",
|
|
"object-src 'none'",
|
|
].join("; ");
|
|
|
|
// Same-origin embedding for inline document previews (PDF viewer in the import review
|
|
// mask, document previews). Everything else stays frame-ancestors 'none' / DENY.
|
|
const cspEmbeddable = csp.replace("frame-ancestors 'none'", "frame-ancestors 'self'");
|
|
|
|
// Routes that stream stored files and may be shown in a same-origin <iframe>.
|
|
const EMBEDDABLE_FILE_ROUTES = ["/files/:path*", "/imports/:id/file"];
|
|
|
|
const securityHeaders = [
|
|
{ key: "Content-Security-Policy", value: csp },
|
|
// HSTS bewusst zusätzlich in der App (neben dem Coolify-/Traefik-Proxy).
|
|
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
|
|
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
{ key: "X-Frame-Options", value: "DENY" },
|
|
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
|
// Einsatz-App: Kamera (Fotos), Mikrofon (Sprachnotizen), Standort (Einsatzstart) —
|
|
// nur für den eigenen Ursprung. Zahlungs-API bleibt aus.
|
|
{ key: "Permissions-Policy", value: "camera=(self), microphone=(self), geolocation=(self), payment=()" },
|
|
];
|
|
|
|
const nextConfig: NextConfig = {
|
|
// Standalone-Output für den Docker-Multi-Stage-Build (siehe Dockerfile)
|
|
output: "standalone",
|
|
experimental: {
|
|
// Proxy (src/proxy.ts) buffers request bodies; the 10 MB default truncates uploads silently.
|
|
// Largest allowed upload is 25 MB (PDF import) plus multipart overhead.
|
|
proxyClientMaxBodySize: "26mb",
|
|
},
|
|
// i18n-Kataloge werden zur Laufzeit per fs geladen (src/i18n/request.ts) — für den
|
|
// standalone-Output explizit mitkopieren.
|
|
outputFileTracingIncludes: {
|
|
"/*": ["./messages/**/*.json"],
|
|
},
|
|
async headers() {
|
|
// Later entries override same-named headers of earlier matches (Next.js header semantics).
|
|
return [
|
|
{ source: "/:path*", headers: securityHeaders },
|
|
...EMBEDDABLE_FILE_ROUTES.map((source) => ({
|
|
source,
|
|
headers: [
|
|
{ key: "Content-Security-Policy", value: cspEmbeddable },
|
|
{ key: "X-Frame-Options", value: "SAMEORIGIN" },
|
|
],
|
|
})),
|
|
];
|
|
},
|
|
};
|
|
|
|
const withNextIntl = createNextIntlPlugin();
|
|
|
|
export default withNextIntl(nextConfig);
|