From 53e2873fd4344c4d551151fb759f62741cfedd71 Mon Sep 17 00:00:00 2001 From: null Date: Sun, 17 May 2026 21:51:53 -0500 Subject: [PATCH] fix: honeypot spam protection, 409 conflict handling (#119 #126) (batch 9.5) --- server/index.js | 16 ++++++++++++++++ src/pages/Contact.jsx | 19 ++++++++++++++++++- src/pages/Support.jsx | 12 ++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/server/index.js b/server/index.js index e480f85..fc89bbe 100644 --- a/server/index.js +++ b/server/index.js @@ -243,6 +243,7 @@ const leadSchema = z.object({ zip: z.string().trim().max(10, 'ZIP code must be 10 characters or less').optional().or(z.literal('').transform(() => undefined)), message: z.string().trim().max(5000, 'Message must be 5000 characters or less').optional().or(z.literal('').transform(() => undefined)), service_interest: z.string().trim().max(50, 'Service interest must be 50 characters or less').optional().or(z.literal('').transform(() => undefined)), + company_website: z.string().optional(), // Honeypot field - bots fill this, humans don't see it }) const supportSchema = z.object({ @@ -254,6 +255,7 @@ const supportSchema = z.object({ priority: z.enum(['low', 'medium', 'high'], { errorMap: () => ({ message: 'Priority must be low, medium, or high' }), }).transform((val) => val?.toLowerCase() ?? undefined).optional().or(z.literal('').transform(() => undefined)), + company_website: z.string().optional(), // Honeypot field - bots fill this, humans don't see it }) // --- Zoho CRM Forwarding (best-effort, fire-and-forget) --- @@ -532,6 +534,13 @@ app.get('/api/health', (req, res) => { // Submit lead app.post('/api/leads', express.json({ limit: '1mb' }), (req, res) => { + // Honeypot check - if filled, it's a bot + if (req.body.company_website) { + log.info('[Spam] Honeypot triggered, ignoring submission') + // Return success to bot so it doesn't retry + return res.json({ success: true, message: "Thanks! We'll be in touch shortly." }) + } + let sanitized try { const parsed = leadSchema.safeParse(req.body) @@ -603,6 +612,13 @@ app.post('/api/leads', express.json({ limit: '1mb' }), (req, res) => { // Submit support request app.post('/api/support', express.json({ limit: '1mb' }), (req, res) => { + // Honeypot check - if filled, it's a bot + if (req.body.company_website) { + log.info('[Spam] Honeypot triggered, ignoring submission') + // Return success to bot so it doesn't retry + return res.json({ success: true, message: "Thanks! We'll get back to you soon." }) + } + try { const parsed = supportSchema.safeParse(req.body) diff --git a/src/pages/Contact.jsx b/src/pages/Contact.jsx index 7eca98e..41d21a4 100644 --- a/src/pages/Contact.jsx +++ b/src/pages/Contact.jsx @@ -19,6 +19,7 @@ const Contact = () => { zip: '', message: '', service_interest: '', + company_website: '', // Honeypot field - hidden from humans, bots will fill it }) const [errors, setErrors] = useState({ company: '', @@ -50,7 +51,12 @@ const Contact = () => { }) }, onError: (error) => { - toast.error(error.message || 'Failed to submit form. Please try again.') + // 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.') + } }, }) @@ -326,6 +332,17 @@ const Contact = () => { )} + +