From 9b47668fe986b38aadea9277589228fcacd485cc Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 21 Jul 2026 20:27:08 +0200 Subject: [PATCH] chore: CSP script nonces, deploy health check, dead-code cleanup Add per-request CSP nonces (drop script unsafe-inline), post-deploy /api/health gate, bump next-auth to beta.32, and remove unused motion/cache/permission helpers. Co-authored-by: Cursor --- .gitea/workflows/deploy.yaml | 20 +++++++ next.config.ts | 16 +---- package.json | 2 +- pnpm-lock.yaml | 22 +++---- src/actions/save-logo.ts | 53 ----------------- src/app/(site)/register/page.tsx | 3 + src/app/layout.tsx | 8 ++- src/components/auth/register-form.tsx | 25 +++++++- src/components/motion-reveal.tsx | 31 ---------- src/lib/admin-helpers.ts | 23 ------- src/lib/api-response.ts | 41 ------------- src/lib/cache.ts | 5 -- src/lib/csp.test.ts | 20 +++++++ src/lib/csp.ts | 36 +++++++++++ src/lib/deploy-workflow-contract.test.ts | 12 ++++ src/lib/motion.ts | 76 ------------------------ src/lib/permissions.ts | 76 ------------------------ src/lib/redis-cache.ts | 10 ---- src/lib/redis.ts | 14 ----- src/proxy.ts | 6 ++ 20 files changed, 141 insertions(+), 358 deletions(-) create mode 100644 src/lib/csp.test.ts create mode 100644 src/lib/csp.ts diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 961f669f..5fc9390e 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -309,4 +309,24 @@ jobs: exit 1 fi + echo "Waiting for HTTP health check..." + HEALTH_URL="${DEPLOY_HEALTH_URL:-http://127.0.0.1:3000/api/health}" + HEALTH_OK=0 + for i in $(seq 1 15); do + BODY="$(curl -sf --max-time 5 "${HEALTH_URL}" 2>/dev/null || true)" + if echo "${BODY}" | grep -q '"database":true'; then + echo "Health OK (${HEALTH_URL})" + HEALTH_OK=1 + break + fi + echo "Health attempt ${i}/15 failed, retrying..." + sleep 2 + done + if [ "${HEALTH_OK}" != "1" ]; then + echo "ERROR: Health check failed after deploy (${HEALTH_URL})" >&2 + echo "Last body: ${BODY:-}" >&2 + journalctl -u atom-nexst.service -n 40 --no-pager >&2 || true + exit 1 + fi + echo "--- Deployed successfully ---" diff --git a/next.config.ts b/next.config.ts index 0539e5ea..4b3ca4cc 100644 --- a/next.config.ts +++ b/next.config.ts @@ -16,21 +16,7 @@ const securityHeaders = [ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=(), interest-cohort=()", }, - { - key: "Content-Security-Policy", - value: [ - "default-src 'self'", - "script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://static.cloudflareinsights.com", - "style-src 'self' 'unsafe-inline'", - "img-src 'self' data: blob: https:", - "frame-src 'self' https://challenges.cloudflare.com https://www.google.com/recaptcha/", - "connect-src 'self' https: wss:", - "font-src 'self' data:", - "object-src 'none'", - "base-uri 'self'", - "form-action 'self'", - ].join("; "), - }, + // CSP is set per-request in src/proxy.ts with a script nonce (no 'unsafe-inline' for scripts). ]; const nextConfig: NextConfig = { diff --git a/package.json b/package.json index 00989b95..0373a26c 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "music-metadata": "^11.14.0", "mysql2": "^3.23.0", "next": "^16.2.11", - "next-auth": "5.0.0-beta.31", + "next-auth": "5.0.0-beta.32", "next-intl": "^4.13.3", "next-view-transitions": "^0.3.5", "nodemailer": "^7.0.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0acb2faf..0cfc3d9c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,8 +92,8 @@ importers: specifier: ^16.2.11 version: 16.2.11(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) next-auth: - specifier: 5.0.0-beta.31 - version: 5.0.0-beta.31(next@16.2.11(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(nodemailer@7.0.13)(react@19.2.8) + specifier: 5.0.0-beta.32 + version: 5.0.0-beta.32(next@16.2.11(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(nodemailer@7.0.13)(react@19.2.8) next-intl: specifier: ^4.13.3 version: 4.13.3(next@16.2.11(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@5.9.3) @@ -221,12 +221,12 @@ packages: '@apm-js-collab/tracing-hooks@0.13.0': resolution: {integrity: sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==} - '@auth/core@0.41.2': - resolution: {integrity: sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==} + '@auth/core@0.41.3': + resolution: {integrity: sha512-sJ3JMHHkXMD3aOjopv7mOBTO1Ocw4b0fAEXJBz6k7YHLpYQI6C40jCUPc5fNvUKxXRXNE1/sRISA15UrwWJBTw==} peerDependencies: '@simplewebauthn/browser': ^9.0.1 '@simplewebauthn/server': ^9.0.2 - nodemailer: ^7.0.7 + nodemailer: ^7.0.7 || ^8.0.5 peerDependenciesMeta: '@simplewebauthn/browser': optional: true @@ -3508,13 +3508,13 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next-auth@5.0.0-beta.31: - resolution: {integrity: sha512-1OBgCKPzo+S7UWWMp3xgvGvIJ0OpV7B3vR4ZDRqD9a4Ch+OT6dakLXG9ivhtmIWVa71nTSXattOHyCg8sNi8/Q==} + next-auth@5.0.0-beta.32: + resolution: {integrity: sha512-CGlChIEWZ6LltNVxrE5yiySMID+Idpmry47JYA5lLwgD8Sx02a8M65VL0TWVz9nbnOioS/tCW/rP/0+mE7Qp4Q==} peerDependencies: '@simplewebauthn/browser': ^9.0.1 '@simplewebauthn/server': ^9.0.2 next: ^14.0.0-0 || ^15.0.0 || ^16.0.0 - nodemailer: ^7.0.7 + nodemailer: ^7.0.7 || ^8.0.5 react: ^18.2.0 || ^19.0.0 peerDependenciesMeta: '@simplewebauthn/browser': @@ -4325,7 +4325,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@auth/core@0.41.2(nodemailer@7.0.13)': + '@auth/core@0.41.3(nodemailer@7.0.13)': dependencies: '@panva/hkdf': 1.2.1 jose: 6.2.3 @@ -7123,9 +7123,9 @@ snapshots: neo-async@2.6.2: {} - next-auth@5.0.0-beta.31(next@16.2.11(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(nodemailer@7.0.13)(react@19.2.8): + next-auth@5.0.0-beta.32(next@16.2.11(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(nodemailer@7.0.13)(react@19.2.8): dependencies: - '@auth/core': 0.41.2(nodemailer@7.0.13) + '@auth/core': 0.41.3(nodemailer@7.0.13) next: 16.2.11(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 optionalDependencies: diff --git a/src/actions/save-logo.ts b/src/actions/save-logo.ts index 70125c09..af398622 100644 --- a/src/actions/save-logo.ts +++ b/src/actions/save-logo.ts @@ -58,56 +58,3 @@ export async function saveLogo( }; } } - -export async function saveLogoFromUrl( - gifUrl: string, -): Promise<{ success: boolean; url?: string; error?: string }> { - try { - const res = await fetch(gifUrl); - if (!res.ok) - return { success: false, error: `Failed to fetch GIF: ${res.status}` }; - - const contentType = res.headers.get("content-type") ?? "image/gif"; - const buffer = Buffer.from(await res.arrayBuffer()); - - const ext = - contentType === "image/png" - ? "png" - : contentType === "image/gif" - ? "gif" - : contentType === "image/jpeg" - ? "jpg" - : contentType === "image/webp" - ? "webp" - : "gif"; - const filename = `logo-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`; - const baseDir = MEDIA_DIR; - const filePath = path.resolve(baseDir, filename); - if (!filePath.startsWith(baseDir + path.sep)) { - return { success: false, error: "Invalid path" }; - } - - // eslint-disable-next-line security/detect-non-literal-fs-filename - await mkdir(baseDir, { recursive: true }); - // eslint-disable-next-line security/detect-non-literal-fs-filename - await writeFile(filePath, buffer); - - const url = `/api/media/logo/${filename}`; - - await prisma.websiteSetting.upsert({ - where: { key: "cms_logo" }, - update: { value: url }, - create: { key: "cms_logo", value: url, comment: "Logo (generator)" }, - }); - - siteSettings.reload(); - revalidatePath("/", "layout"); - - return { success: true, url }; - } catch (e) { - return { - success: false, - error: e instanceof Error ? e.message : "Unknown error", - }; - } -} diff --git a/src/app/(site)/register/page.tsx b/src/app/(site)/register/page.tsx index 6ce500c9..a0099cfd 100644 --- a/src/app/(site)/register/page.tsx +++ b/src/app/(site)/register/page.tsx @@ -1,4 +1,5 @@ import { getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; import { RegisterForm } from "@/components/auth/register-form"; import { captchaConfig } from "@/lib/services/captcha"; import { siteSettings } from "@/lib/services/site-settings"; @@ -12,6 +13,7 @@ export default async function RegisterPage({ }) { const t = await getTranslations("pages.register"); const { error } = await searchParams; + const nonce = (await headers()).get("x-nonce") ?? undefined; const [hotelName, captcha] = await Promise.all([ siteSettings.get("hotel_name", "Atom"), @@ -26,6 +28,7 @@ export default async function RegisterPage({ hotelName={hotelName ?? "Atom"} captcha={captcha} error={error} + nonce={nonce} /> diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9dc6ae66..dc88601f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Nunito, Pixelify_Sans } from "next/font/google"; +import { headers } from "next/headers"; import Script from "next/script"; import { NextIntlClientProvider } from "next-intl"; import { getLocale, getMessages } from "next-intl/server"; @@ -61,6 +62,7 @@ export default async function RootLayout({ const messages = await getMessages(); const defaultDark = await siteSettings.getBool("default_dark", false); const nitroUrl = await siteSettings.get("nitro_client_url", ""); + const nonce = (await headers()).get("x-nonce") ?? undefined; return ( -