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)), 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)), 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)), 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({ const supportSchema = z.object({
@ -254,6 +255,7 @@ const supportSchema = z.object({
priority: z.enum(['low', 'medium', 'high'], { priority: z.enum(['low', 'medium', 'high'], {
errorMap: () => ({ message: 'Priority must be low, medium, or high' }), errorMap: () => ({ message: 'Priority must be low, medium, or high' }),
}).transform((val) => val?.toLowerCase() ?? undefined).optional().or(z.literal('').transform(() => undefined)), }).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) --- // --- Zoho CRM Forwarding (best-effort, fire-and-forget) ---
@ -532,6 +534,13 @@ app.get('/api/health', (req, res) => {
// Submit lead // Submit lead
app.post('/api/leads', express.json({ limit: '1mb' }), (req, res) => { 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 let sanitized
try { try {
const parsed = leadSchema.safeParse(req.body) const parsed = leadSchema.safeParse(req.body)
@ -603,6 +612,13 @@ app.post('/api/leads', express.json({ limit: '1mb' }), (req, res) => {
// Submit support request // Submit support request
app.post('/api/support', express.json({ limit: '1mb' }), (req, res) => { 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 { try {
const parsed = supportSchema.safeParse(req.body) const parsed = supportSchema.safeParse(req.body)

View File

@ -19,6 +19,7 @@ const Contact = () => {
zip: '', zip: '',
message: '', message: '',
service_interest: '', service_interest: '',
company_website: '', // Honeypot field - hidden from humans, bots will fill it
}) })
const [errors, setErrors] = useState({ const [errors, setErrors] = useState({
company: '', company: '',
@ -50,7 +51,12 @@ const Contact = () => {
}) })
}, },
onError: (error) => { 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.') toast.error(error.message || 'Failed to submit form. Please try again.')
}
}, },
}) })
@ -326,6 +332,17 @@ const Contact = () => {
)} )}
</div> </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 <Button
type="submit" type="submit"
className="w-full" className="w-full"

View File

@ -18,6 +18,7 @@ const Support = () => {
phone: '', phone: '',
issue: '', issue: '',
priority: 'medium', priority: 'medium',
company_website: '', // Honeypot field - hidden from humans, bots will fill it
}) })
const [errors, setErrors] = useState({ const [errors, setErrors] = useState({
name: '', name: '',
@ -338,6 +339,17 @@ const Support = () => {
)} )}
</div> </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 <Button
type="submit" type="submit"
className="w-full" className="w-full"