This commit is contained in:
parent
00f5356db4
commit
53e2873fd4
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue