Two settings were wrong for a platform build. output: "standalone" tells Next.js to emit a self-contained server, which is what the Dockerfile copies in — and what Vercel neither needs nor expects, since it builds and packages the app itself. It is now conditional on the VERCEL variable, which every build there sets, so each path gets what it wants. Verified both ways: with VERCEL=1 no standalone directory appears, without it one does. /api/import declared maxDuration = 120. The free tier caps at 60 and refuses anything higher, so the deployment would have failed on a value chosen for a self-hosted server. Lowered, with the reason and the Pro ceiling written next to it. DEPLOYMENT.md now covers both paths, and says plainly that the repository cannot be connected: git.elycon.solutions is self-hosted, and Vercel's git integration only speaks GitHub, GitLab and Bitbucket. Deploying from the workstation with the CLI works with any repository and is the shorter road; mirroring to GitHub is written down as the alternative, with its cost — two remotes to keep in step. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
73 lines
3.1 KiB
TypeScript
73 lines
3.1 KiB
TypeScript
import type { NextConfig } from "next";
|
|
|
|
// Report-only rather than enforcing, deliberately: the policy is derived from
|
|
// what this app is known to load — its own bundle, the self-hosted Nunito
|
|
// files from next/font, and nothing else — but an unenforced policy that logs
|
|
// violations is worth more than a guessed one that blanks the app for every
|
|
// HR user. Promote it to `Content-Security-Policy` once the reports come back
|
|
// clean.
|
|
//
|
|
// 'unsafe-inline' on script-src is not laziness: Next.js inlines its
|
|
// bootstrap and hydration payload as inline <script> tags, so a nonce-based
|
|
// policy means threading a per-request nonce through proxy.ts — a separate
|
|
// change, and the reason this starts in report-only.
|
|
function contentSecurityPolicy(): string {
|
|
// Die Anmeldung schickt ein Formular an /api/auth/signin/…, und von dort
|
|
// geht es per Weiterleitung zum Anmeldeserver. `form-action` gilt auch für
|
|
// die Weiterleitungen nach einem Formularversand — steht der Aussteller
|
|
// nicht darin, bricht der Browser die Anmeldung ab. Genau hier wäre die
|
|
// Nur-Bericht-Fassung später eine böse Überraschung.
|
|
let issuerOrigin = "";
|
|
try {
|
|
issuerOrigin = new URL(process.env.AUTH_MICROSOFT_ENTRA_ID_ISSUER ?? "").origin;
|
|
} catch {
|
|
issuerOrigin = "";
|
|
}
|
|
const formAction = ["'self'", issuerOrigin].filter(Boolean).join(" ");
|
|
|
|
return [
|
|
"default-src 'self'",
|
|
"script-src 'self' 'unsafe-inline'",
|
|
"style-src 'self' 'unsafe-inline'",
|
|
"img-src 'self' data: blob:",
|
|
"font-src 'self'",
|
|
// Die Anwendung spricht im Browser mit niemandem ausser sich selbst: die
|
|
// Datenbank erreicht nur der Server, und seit die API-Schicht weg ist,
|
|
// gibt es keine Gegenstelle mehr, die von aussen angesprochen würde.
|
|
"connect-src 'self'",
|
|
"frame-ancestors 'self'",
|
|
"base-uri 'self'",
|
|
`form-action ${formAction}`,
|
|
"object-src 'none'",
|
|
].join("; ");
|
|
}
|
|
|
|
const nextConfig: NextConfig = {
|
|
// Emits a self-contained .next/standalone server (only the deps actually
|
|
// used at runtime, no full node_modules) — what the Dockerfile copies in.
|
|
//
|
|
// Auf Vercel ist das falsch: dort baut die Plattform selbst und erwartet
|
|
// die übliche Ausgabe. `VERCEL` setzt sie in jeder Baustrecke, die Angabe
|
|
// entfällt dort also von selbst — und der Docker-Weg bleibt unberührt.
|
|
output: process.env.VERCEL ? undefined : "standalone",
|
|
// Baseline security headers (clickjacking, MIME-sniffing, referrer leakage,
|
|
// browser feature access) plus the report-only CSP described above.
|
|
async headers() {
|
|
return [
|
|
{
|
|
source: "/:path*",
|
|
headers: [
|
|
{ key: "X-Frame-Options", value: "SAMEORIGIN" },
|
|
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
|
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
|
|
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
|
|
{ key: "Content-Security-Policy-Report-Only", value: contentSecurityPolicy() },
|
|
],
|
|
},
|
|
];
|
|
},
|
|
};
|
|
|
|
export default nextConfig;
|