- Restrict meetingPassword field so it isn't leaked via the public REST/GraphQL API - Fix booking slot computation to use Europe/Berlin time instead of the server's UTC clock - Reset email verification when a customer changes their email address - Fix ICS calendar injection via unescaped carriage returns in event descriptions - Bind WebRTC meeting kicks to a persistent client id so they survive a rejoin - Move the meeting signaling token out of the WebSocket query string into the first message - Prevent duplicate signaling connections from leaking when the same participant reconnects - Use the rightmost X-Forwarded-For entry for rate limiting (single trusted reverse proxy) - Use constant-time comparison for the cron endpoint's bearer token - Remove SVG from allowed Media upload types (stored-XSS risk, unused anyway) - Add baseline security headers and a CSP to next.config.ts - Fix CI/CD script injection via unsanitized git ref names in release.yml
78 lines
3.2 KiB
TypeScript
78 lines
3.2 KiB
TypeScript
import { withPayload } from "@payloadcms/next/withPayload";
|
|
import type { NextConfig } from "next";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const dirname = path.dirname(__filename);
|
|
|
|
// Applied to every route, including /admin and /api — none of these
|
|
// constrain what the page can load/render, so they can't break the Payload
|
|
// admin bundle. Camera/microphone/display-capture are allowed for the
|
|
// site's own origin because the WebRTC call pages (app/(call)/**) need them.
|
|
const baseSecurityHeaders = [
|
|
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
{ key: "X-Frame-Options", value: "SAMEORIGIN" },
|
|
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
|
{ key: "Permissions-Policy", value: "camera=(self), microphone=(self), display-capture=(self), geolocation=(), payment=()" },
|
|
// Browsers only ever honor this over an actual HTTPS connection, so it's
|
|
// harmless to send in local http:// dev too.
|
|
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
|
|
];
|
|
|
|
// Deliberately NOT applied to /admin or /api: Payload's admin bundle isn't
|
|
// audited here for CSP compatibility, and API routes serve data, not HTML,
|
|
// so a content policy doesn't apply to them anyway. connect-src stays broad
|
|
// (rather than 'self') because the WebRTC call pages connect to whatever
|
|
// STUN/TURN server is configured via env vars at runtime (see
|
|
// lib/meeting/iceServers.ts) — that host isn't known here. style-src needs
|
|
// 'unsafe-inline' because framer-motion drives its animations through
|
|
// inline style attributes. script-src allows 'unsafe-eval' only in dev,
|
|
// matching Next's own documented CSP guidance — React uses eval there for
|
|
// server-error-stack reconstruction; neither React nor Next use it in a
|
|
// production build.
|
|
const isDev = process.env.NODE_ENV === "development";
|
|
const frontendCSP = [
|
|
"default-src 'self'",
|
|
`script-src 'self'${isDev ? " 'unsafe-eval'" : ""}`,
|
|
"style-src 'self' 'unsafe-inline'",
|
|
"img-src 'self' data: blob:",
|
|
"font-src 'self' data:",
|
|
"connect-src 'self' https: wss: stun: turn: turns:",
|
|
"media-src 'self' blob:",
|
|
"frame-ancestors 'self'",
|
|
"base-uri 'self'",
|
|
"form-action 'self'",
|
|
"object-src 'none'",
|
|
"upgrade-insecure-requests",
|
|
].join("; ");
|
|
|
|
const nextConfig: NextConfig = {
|
|
// NOT "standalone": this project uses a custom server (server.ts) for the
|
|
// WebRTC signaling WebSocket, and standalone output can't be combined with
|
|
// a custom server. The Dockerfile runs the full app via `npm start` instead.
|
|
images: {
|
|
localPatterns: [
|
|
{
|
|
pathname: "/api/media/file/**",
|
|
},
|
|
],
|
|
},
|
|
// Two root layouts exist — app/(frontend) and app/(payload) — so a single
|
|
// not-found.tsx can't compose a 404; see app/global-not-found.tsx.
|
|
experimental: {
|
|
globalNotFound: true,
|
|
},
|
|
turbopack: {
|
|
root: path.resolve(dirname),
|
|
},
|
|
async headers() {
|
|
return [
|
|
{ source: "/:path*", headers: baseSecurityHeaders },
|
|
{ source: "/((?!admin|api).*)", headers: [{ key: "Content-Security-Policy", value: frontendCSP }] },
|
|
];
|
|
},
|
|
};
|
|
|
|
export default withPayload(nextConfig, { devBundleServerPackages: false });
|