From 2c002c2f824bb92d7ebaa0c7342594062a50bb02 Mon Sep 17 00:00:00 2001 From: null Date: Sun, 17 May 2026 22:33:11 -0500 Subject: [PATCH] fix: remove React Query, add HTTPS redirect, document CSP Zoho note (#128 #127 #129) (batch 10.0) --- package-lock.json | 31 ++---------------- package.json | 1 - server/index.js | 14 ++++++++ src/lib/api.js | 32 ++++++++++++++++++- src/main.jsx | 15 ++------- src/pages/Contact.jsx | 74 ++++++++++++++++++++++--------------------- src/pages/Support.jsx | 62 ++++++++++++++++++------------------ 7 files changed, 119 insertions(+), 110 deletions(-) diff --git a/package-lock.json b/package-lock.json index 94f1dde..1c3a43c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,15 @@ { "name": "queuenorth-website", - "version": "0.6.6", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "queuenorth-website", - "version": "0.6.6", + "version": "0.7.0", "dependencies": { "@radix-ui/react-dialog": "^1.1.0", "@radix-ui/react-visually-hidden": "^1.2.4", - "@tanstack/react-query": "^5.62.0", "better-sqlite3": "^11.8.0", "cors": "^2.8.6", "express": "^4.21.2", @@ -1646,32 +1645,6 @@ "win32" ] }, - "node_modules/@tanstack/query-core": { - "version": "5.100.10", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz", - "integrity": "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.100.10", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.10.tgz", - "integrity": "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.100.10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", diff --git a/package.json b/package.json index 955f9b4..cb63376 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "dependencies": { "@radix-ui/react-dialog": "^1.1.0", "@radix-ui/react-visually-hidden": "^1.2.4", - "@tanstack/react-query": "^5.62.0", "better-sqlite3": "^11.8.0", "cors": "^2.8.6", "express": "^4.21.2", diff --git a/server/index.js b/server/index.js index 0b649b1..f2aa5b9 100644 --- a/server/index.js +++ b/server/index.js @@ -74,6 +74,10 @@ const cspDirectives = { formAction: ["'self'"], } +// Note: connectSrc currently allows 'self' only. Zoho API calls are server-to-server +// and are not affected by CSP. If client-side Zoho calls are added in the future, +// add Zoho domains here (e.g., 'https://www.zohoapis.com', 'https://accounts.zoho.com') + app.use(helmet({ contentSecurityPolicy: { directives: cspDirectives, @@ -95,6 +99,16 @@ app.use(helmet({ log.info('[Security] Helmet enabled with CSP configured') +// Redirect HTTP to HTTPS in production +if (process.env.NODE_ENV === 'production') { + app.use((req, res, next) => { + if (req.headers['x-forwarded-proto'] === 'http') { + return res.redirect(301, `https://${req.headers.host}${req.url}`) + } + next() + }) +} + // --- CORS Configuration --- const corsOrigin = process.env.CORS_ORIGIN || 'https://queuenorth.com' // Default to production domain const corsConfig = cors({ diff --git a/src/lib/api.js b/src/lib/api.js index c3a64ea..7a4cf75 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -1,6 +1,36 @@ const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api' -// Exponential backoff retry helper +export async function submitLead(data) { + const response = await fetch(`${API_BASE_URL}/leads`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + const error = new Error(errorData.error || `API error: ${response.status}`) + error.response = { status: response.status } + throw error + } + return response.json() +} + +export async function submitSupport(data) { + const response = await fetch(`${API_BASE_URL}/support`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + const error = new Error(errorData.error || `API error: ${response.status}`) + error.response = { status: response.status } + throw error + } + return response.json() +} + +// Exponential backoff retry helper (deprecated, kept for other API calls) const retryFetch = async (fn, { maxRetries = 3, baseDelay = 1000 } = {}) => { let lastError for (let attempt = 0; attempt <= maxRetries; attempt++) { diff --git a/src/main.jsx b/src/main.jsx index 2fc67e2..554883e 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,28 +1,17 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { RouterProvider } from 'react-router-dom' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { Toaster } from 'sonner' import { HelmetProvider } from 'react-helmet-async' import router from './router.jsx' import App from './App.jsx' -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000 * 60 * 5, // 5 minutes - }, - }, -}) - // Wrap the router with providers const Root = () => ( - - - - + + ) diff --git a/src/pages/Contact.jsx b/src/pages/Contact.jsx index 3c715f9..f9c931c 100644 --- a/src/pages/Contact.jsx +++ b/src/pages/Contact.jsx @@ -1,13 +1,12 @@ import { Helmet } from 'react-helmet-async' import { useState } from 'react' -import { useMutation } from '@tanstack/react-query' import { toast } from 'sonner' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { Textarea } from '@/components/ui/Textarea' import { Select } from '@/components/ui/Select' import { Link } from 'react-router-dom' -import { api } from '@/lib/api' +import { submitLead } from '@/lib/api' import { useDebounce } from '@/hooks/useDebounce' const Contact = () => { @@ -29,36 +28,7 @@ const Contact = () => { }) // Debounce validation errors so they don't flash on every keystroke const debouncedErrors = useDebounce(errors, 300) - - const mutation = useMutation({ - mutationFn: (data) => api.post('/leads', data), - onSuccess: () => { - toast.success('Thanks! We\'ll be in touch shortly.') - setFormState({ - company: '', - name: '', - email: '', - phone: '', - zip: '', - message: '', - service_interest: '', - }) - setErrors({ - company: '', - name: '', - email: '', - message: '', - }) - }, - onError: (error) => { - // 409 means duplicate email - this is actually good news - if (error.response?.status === 409) { - toast.success("We already have your submission! We'll be in touch.") - } else { - toast.error(error.message || 'Failed to submit form. Please try again.') - } - }, - }) + const [isSubmitting, setIsSubmitting] = useState(false) const validateForm = () => { const newErrors = { @@ -95,7 +65,39 @@ const Contact = () => { const handleSubmit = (e) => { e.preventDefault() if (!validateForm()) return - mutation.mutate(formState) + handleSubmitForm() + } + + const handleSubmitForm = async () => { + setIsSubmitting(true) + try { + await submitLead(formState) + toast.success("Thanks! We\'ll be in touch shortly.") + setFormState({ + company: '', + name: '', + email: '', + phone: '', + zip: '', + message: '', + service_interest: '', + }) + setErrors({ + company: '', + name: '', + email: '', + message: '', + }) + } catch (error) { + // 409 means duplicate email - this is actually good news + if (error.response?.status === 409) { + toast.success("We already have your submission! We\'ll be in touch.") + } else { + toast.error(error.message || 'Failed to submit form. Please try again.') + } + } finally { + setIsSubmitting(false) + } } const handleChange = (e) => { @@ -206,7 +208,7 @@ const Contact = () => { {/* Right - Form */}
-
+
diff --git a/src/pages/Support.jsx b/src/pages/Support.jsx index bbbebcb..46cda63 100644 --- a/src/pages/Support.jsx +++ b/src/pages/Support.jsx @@ -1,13 +1,12 @@ import { Helmet } from 'react-helmet-async' import { useState } from 'react' -import { useMutation } from '@tanstack/react-query' import { toast } from 'sonner' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { Textarea } from '@/components/ui/Textarea' import { Select } from '@/components/ui/Select' import { Link } from 'react-router-dom' -import { api } from '@/lib/api' +import { submitSupport } from '@/lib/api' import { useDebounce } from '@/hooks/useDebounce' const Support = () => { @@ -28,30 +27,7 @@ const Support = () => { }) // Debounce validation errors so they don't flash on every keystroke const debouncedErrors = useDebounce(errors, 300) - - const mutation = useMutation({ - mutationFn: (data) => api.post('/support', data), - onSuccess: () => { - toast.success('Thanks! We\'ll get back to you soon.') - setFormState({ - name: '', - company: '', - email: '', - phone: '', - issue: '', - priority: 'medium', - }) - setErrors({ - name: '', - company: '', - email: '', - issue: '', - }) - }, - onError: (error) => { - toast.error(error.message || 'Failed to submit form. Please try again.') - }, - }) + const [isSubmitting, setIsSubmitting] = useState(false) const validateForm = () => { const newErrors = { @@ -93,7 +69,33 @@ const Support = () => { const handleSubmit = (e) => { e.preventDefault() if (!validateForm()) return - mutation.mutate(formState) + handleSubmitForm() + } + + const handleSubmitForm = async () => { + setIsSubmitting(true) + try { + await submitSupport(formState) + toast.success("Thanks! We\'ll get back to you soon.") + setFormState({ + name: '', + company: '', + email: '', + phone: '', + issue: '', + priority: 'medium', + }) + setErrors({ + name: '', + company: '', + email: '', + issue: '', + }) + } catch (error) { + toast.error(error.message || 'Failed to submit form. Please try again.') + } finally { + setIsSubmitting(false) + } } const handleChange = (e) => { @@ -232,7 +234,7 @@ const Support = () => { {/* Right - Form */}
-
+