Files
tessera-ctl/apps/web/next.config.ts
T
schalli bf632d90a5 fix(quick-261009-p0m): Review-Befunde WR-01/02/04/05, IN-01/02/03/04 (Abbild-Bau, Semgrep im Container, Zeitbudget, ZAP-Haken)
- WR-01: prisma generate als eigener Schritt mit Pruefung im api-Abbild, Bau scheitert ohne Client; Startprobe als .gitea/scripts/image-start-check.sh versioniert
- WR-02: Semgrep im offiziellen Container, per Digest angepinnt (kein pipx/PyPI mehr)
- WR-04: Zeitbudget 1500 s, Limits je Werkzeug, Ergebniszeile sofort nach jedem Werkzeug
- WR-05: Authorization-Ersetzung der ZAP-Pruefung nur fuer das Ziel
- IN-01: yarn/npm per Platzhalter entfernt und Abwesenheit geprueft (api und web)
- IN-02: Kommentar zu doppelten Kopfzeilen in next.config.ts richtiggestellt
- IN-03: Berichtsordner Modus 700, Zugangsdaten nur als base64 (keine curl-Konfiguration)
- IN-04: flacher Klon wird bei gitleaks als unvollstaendig gemeldet

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 20:59:12 +02:00

80 lines
3.7 KiB
TypeScript

import { readFileSync } from 'node:fs';
import path from 'node:path';
import type { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
// Bauzeit-Einbettung der Aenderungsliste (quick-260916-dcz): CHANGELOG.md aus
// dem Wurzelverzeichnis wird hier gelesen und als `env.TESSERA_CHANGELOG_MD`
// abgelegt. Next.js ersetzt `process.env.TESSERA_CHANGELOG_MD` fuer webpack UND
// Turbopack ueber denselben Define-Mechanismus. Der Text bleibt nur im
// Server-Bundle, weil ausschliesslich Server-Code `@/lib/changelog` importiert:
// die Server-Seite (changelog/page.tsx) und die 'use server'-Datei
// lib/release-notice-actions.ts (quick-260925-bow) — Importdisziplin, kein
// Client-Chunk.
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
function readChangelog(): string {
const file = path.resolve(__dirname, '../../CHANGELOG.md');
try {
return readFileSync(file, 'utf8');
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(
`CHANGELOG.md konnte nicht gelesen werden (${file}): ${reason}. ` +
'Die Datei muss im Wurzelverzeichnis liegen; im Docker-Bau braucht sie die Ausnahme ' +
'`!CHANGELOG.md` in .dockerignore und die Zeile `COPY CHANGELOG.md ./` in apps/web/Dockerfile.',
);
}
}
// Schutz-Kopfzeilen der Anwendung (quick-261009-p0m, aus dem ersten ZAP-Lauf). Sie gelten fuer
// alle Pfade und stehen hier, damit sie auch ohne vorgeschalteten Proxy gesendet werden.
// Setzt der Proxy dieselben Kopfzeilen mit gleichem Wert, aendert sich nichts. Setzt er sie mit
// ANDEREM Wert (etwa eine eigene Content-Security-Policy oder X-Frame-Options: DENY), gelten
// beide nebeneinander und der Browser nimmt die strengere -- das kann Tessera-Seiten sperren,
// die sich selbst einrahmen. Ob der Proxy welche sendet, ist mit dem ersten ZAP-Lauf gegen die
// oeffentliche Adresse zu pruefen (docs/sicherheitsprotokoll.md).
// Absichtlich KEINE vollstaendige Content-Security-Policy -- sie braucht eine
// eigene Pruefung der Inline-Skripte von Next.js und bleibt im Sicherheitsprotokoll "offen".
// `frame-ancestors 'self'` ist nur die eine Richtlinie gegen das Einrahmen durch fremde Seiten
// (Gegenstueck zu X-Frame-Options fuer neuere Browser). Eingebettete FREMDE Seiten (XFrame-Kachel,
// eigene Module) sind nicht betroffen: diese Kopfzeilen schuetzen nur Tessera selbst.
// Permissions-Policy sperrt Kamera, Mikrofon, Standort, Zahlung und USB; die Zwischenablage
// (clipboard-write fuer "Link kopieren"), Vollbild und Benachrichtigungen bleiben unberuehrt.
// Cross-Origin-Opener-Policy `same-origin-allow-popups`: jeder Link in ein neues Fenster
// (Nextcloud-Anmeldung, Favoriten, ...) traegt noopener, nichts nutzt window.opener/postMessage.
const securityHeaders = [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'Content-Security-Policy', value: "frame-ancestors 'self'" },
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), payment=(), usb=()',
},
{ key: 'Cross-Origin-Opener-Policy', value: 'same-origin-allow-popups' },
];
const nextConfig: NextConfig = {
output: 'standalone' as const,
poweredByHeader: false,
env: {
TESSERA_CHANGELOG_MD: readChangelog(),
},
async headers() {
return [{ source: '/:path*', headers: securityHeaders }];
},
async rewrites() {
const apiUrl = process.env.API_INTERNAL_URL || 'http://api:3001';
return [
{
source: '/api-proxy/:path*',
destination: `${apiUrl}/:path*`,
},
];
},
};
export default withNextIntl(nextConfig);