Files
craftvia/next.config.ts
T
msolarczekandClaude Opus 5 ee17134863 L13 Planung: Menüpunkt Planung mit Plantafel und Live-Lage
Plantafel: Standard heute + nächste 4 Werktage (Kolonnen als Zeilen, heutige Spalte mit Live-Status), Heute mit Kolonnen als parallelen Spalten (6–20 Uhr), Woche/nächste Woche, Auslastung, Konflikte/Hinweise, Verzugs- und Gefährdungs-Badges, Drag & Drop (@dnd-kit/core) mit Bestätigungs-Popover und Tastatur-Alternative, ungeplante Aufträge mit Vorschlägen, früher fertig mit Vorziehen, Kolonnenkapazität-Popup. Live-Lage: Leaflet/OSM-Karte mit einem Marker je Kolonne, Liste, Polling 30 s, Hinweis keine GPS-Ortung; CSP img-src für Kacheln. Dashboard-Kacheln Planung heute und Konflikte diese Woche, Einsatz-Vorschläge im Auftragsdetail, Navigation mit Unterpunkten, Smoke-Prüfungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:13:50 +02:00

102 lines
4.3 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";
// L13 Planung: map tiles of the live map (MAP_TILE_URL, default OpenStreetMap). Only the tile host(s)
// are added to img-src — evaluated at build time, so set MAP_TILE_URL before `next build`.
function mapTileSources(): string {
const raw = process.env.MAP_TILE_URL?.trim();
if (!raw) return "https://tile.openstreetmap.org https://*.tile.openstreetmap.org";
const m = /^(https?):\/\/([^/?#]+)/i.exec(raw);
return m ? `${m[1].toLowerCase()}://${m[2].replace(/\{[a-z]\}/gi, "*")}` : "";
}
// 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: ${mapTileSources()}`.trim(),
// 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 },
{
// Browsers must revalidate the service worker on every navigation to pick up updates.
source: "/sw.js",
headers: [
{ key: "Cache-Control", value: "no-cache, no-store, must-revalidate" },
{ key: "Service-Worker-Allowed", value: "/" },
{ key: "Content-Type", value: "application/javascript; charset=utf-8" },
],
},
...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);