fix: honeypot spam protection, 409 conflict handling (#119 #126) (batch 9.5)

This commit is contained in:
null 2026-05-17 21:51:53 -05:00
parent 00f5356db4
commit 53e2873fd4
3 changed files with 46 additions and 1 deletions

View File

@ -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)

View File

@ -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 = () => {
)}
</div>
<div className="absolute opacity-0 h-0 overflow-hidden" aria-hidden="true">
<input
type="text"
name="company_website"
tabIndex="-1"
autoComplete="off"
value={formState.company_website}
onChange={handleChange}
/>
</div>
<Button
type="submit"
className="w-full"

View File

@ -18,6 +18,7 @@ const Support = () => {
phone: '',
issue: '',
priority: 'medium',
company_website: '', // Honeypot field - hidden from humans, bots will fill it
})
const [errors, setErrors] = useState({
name: '',
@ -338,6 +339,17 @@ const Support = () => {
)}
</div>
<div className="absolute opacity-0 h-0 overflow-hidden" aria-hidden="true">
<input
type="text"
name="company_website"
tabIndex="-1"
autoComplete="off"
value={formState.company_website}
onChange={handleChange}
/>
</div>
<Button
type="submit"
className="w-full"