From 05b27d216ace51935af9a65d6383aa36b1219af0 Mon Sep 17 00:00:00 2001 From: null Date: Sun, 14 Jun 2026 15:37:26 -0500 Subject: [PATCH] feat: Dockerfile/docker-compose updates, server improvements, contact form with recaptcha, API integration (batch 0.9.0) --- Dockerfile | 7 ++ docker-compose.yml | 5 + server/index.js | 27 ++++- src/components/RecaptchaPlaceholder.jsx | 104 ++++++++++++++-- src/lib/api.js | 2 +- src/pages/Contact.jsx | 150 ++++++++++++++---------- 6 files changed, 217 insertions(+), 78 deletions(-) diff --git a/Dockerfile b/Dockerfile index b48e2bb..bdfdad1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,10 @@ RUN npm ci # Copy source files COPY . . +# Public Vite values are compiled into the frontend bundle at build time. +ARG VITE_RECAPTCHA_SITE_KEY= +ENV VITE_RECAPTCHA_SITE_KEY=$VITE_RECAPTCHA_SITE_KEY + # Build the frontend RUN npm run build @@ -50,6 +54,9 @@ ENV ZOHO_CLIENT_ID= ENV ZOHO_CLIENT_SECRET= ENV ZOHO_REFRESH_TOKEN= ENV ZOHO_CASES_ENABLED=false +ENV RECAPTCHA_ENABLED=false +ENV RECAPTCHA_SECRET_KEY= +ENV RECAPTCHA_MIN_SCORE=0.5 # Create app directory structure RUN mkdir -p /app/db /app/logs diff --git a/docker-compose.yml b/docker-compose.yml index 2926a2b..f113cb0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ services: build: context: . dockerfile: Dockerfile + args: + - VITE_RECAPTCHA_SITE_KEY=${VITE_RECAPTCHA_SITE_KEY:-} container_name: queuenorth-website ports: - "3001:3001" @@ -26,6 +28,9 @@ services: - ZOHO_CLIENT_SECRET= - ZOHO_REFRESH_TOKEN= - ZOHO_REDIRECT_URI= + - RECAPTCHA_ENABLED=${RECAPTCHA_ENABLED:-false} + - RECAPTCHA_SECRET_KEY=${RECAPTCHA_SECRET_KEY:-} + - RECAPTCHA_MIN_SCORE=${RECAPTCHA_MIN_SCORE:-0.5} restart: unless-stopped # Container runs as non-root user (UID 1001) for security diff --git a/server/index.js b/server/index.js index aba41f5..823cfc1 100644 --- a/server/index.js +++ b/server/index.js @@ -1,7 +1,7 @@ import express from 'express' import path from 'path' import { fileURLToPath } from 'url' -import { existsSync, mkdirSync } from 'fs' +import { existsSync, mkdirSync, readFileSync } from 'fs' import sqlite3 from 'better-sqlite3' import z from 'zod' import rateLimit from 'express-rate-limit' @@ -13,6 +13,27 @@ const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const app = express() +const loadLocalEnv = () => { + const envPath = path.resolve(process.cwd(), '.env') + if (!existsSync(envPath)) return + + const envFile = readFileSync(envPath, 'utf8') + for (const line of envFile.split(/\r?\n/)) { + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) continue + + const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/) + if (!match) continue + + const [, key, rawValue] = match + if (process.env[key] !== undefined) continue + + process.env[key] = rawValue.replace(/^(['"])(.*)\1$/, '$2') + } +} + +loadLocalEnv() + // Trust first proxy (Docker/reverse proxy) for correct client IP in rate limiting app.set('trust proxy', 1) const dbPath = path.join(__dirname, '../db/queuenorth.db') @@ -68,7 +89,9 @@ const cspDirectives = { styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'], fontSrc: ["'self'", 'https://fonts.gstatic.com'], imgSrc: ["'self'", 'data:'], - connectSrc: isDev ? ["'self'", 'ws://localhost:*'] : ["'self'"], + connectSrc: isDev + ? ["'self'", 'ws://localhost:*', 'https://www.google.com/recaptcha/', 'https://www.gstatic.com/recaptcha/', 'https://recaptcha.google.com/recaptcha/'] + : ["'self'", 'https://www.google.com/recaptcha/', 'https://www.gstatic.com/recaptcha/', 'https://recaptcha.google.com/recaptcha/'], frameSrc: ["'self'", 'https://crm.zoho.com', 'https://www.google.com/recaptcha/', 'https://recaptcha.google.com/recaptcha/'], objectSrc: ["'none'"], baseUri: ["'self'"], diff --git a/src/components/RecaptchaPlaceholder.jsx b/src/components/RecaptchaPlaceholder.jsx index bca80ec..2e6a846 100644 --- a/src/components/RecaptchaPlaceholder.jsx +++ b/src/components/RecaptchaPlaceholder.jsx @@ -1,16 +1,96 @@ -const RecaptchaPlaceholder = ({ error = '' }) => ( -
-
-
-

Security verification

-

Google reCAPTCHA placeholder

-
-
-