feat: add captcha verification to contact form

Added math-based captcha system to prevent spam on the contact form:
- Created captcha API endpoint with simple arithmetic questions
- Added captcha UI component with refresh functionality
- Integrated captcha verification into contact form submission
- Relaxed spam filters since captcha provides better protection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-10 17:53:56 +00:00
parent 9158ffa637
commit 989f231d5a
6 changed files with 268 additions and 18 deletions

50
app/api/captcha/route.ts Normal file
View File

@@ -0,0 +1,50 @@
import { NextResponse } from 'next/server'
import { generateCaptcha, verifyCaptcha } from '@/lib/captcha'
export const runtime = 'nodejs'
export async function GET() {
try {
const captchaData = generateCaptcha()
return NextResponse.json({
success: true,
captchaId: captchaData.captchaId,
question: captchaData.question
})
} catch (error) {
console.error('Captcha generation error:', error)
return NextResponse.json({
success: false,
error: 'Failed to generate captcha'
}, { status: 500 })
}
}
export async function POST(request: Request) {
try {
const { captchaId, answer } = await request.json()
if (!captchaId || answer === undefined) {
return NextResponse.json({
success: false,
error: 'Missing captcha ID or answer'
}, { status: 400 })
}
const isValid = verifyCaptcha(captchaId, answer)
return NextResponse.json({
success: true,
valid: isValid
})
} catch (error) {
console.error('Captcha verification error:', error)
return NextResponse.json({
success: false,
error: 'Failed to verify captcha'
}, { status: 500 })
}
}

View File

@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
import { smtpService } from '@/lib/smtp'
import { verifyCaptcha } from '@/lib/captcha'
import { z } from 'zod'
export const runtime = 'nodejs'
@@ -8,7 +9,9 @@ const contactSchema = z.object({
name: z.string().min(1, 'Name is required').max(100),
email: z.string().email('Invalid email address'),
subject: z.string().min(1, 'Subject is required').max(200),
message: z.string().min(10, 'Message must be at least 10 characters').max(5000)
message: z.string().min(10, 'Message must be at least 10 characters').max(5000),
captchaId: z.string().min(1, 'Captcha ID is required'),
captchaAnswer: z.string().min(1, 'Captcha answer is required')
})
export async function POST(request: NextRequest) {
@@ -25,21 +28,26 @@ export async function POST(request: NextRequest) {
}, { status: 400 })
}
const { name, email, subject, message } = validationResult.data
const { name, email, subject, message, captchaId, captchaAnswer } = validationResult.data
// Basic spam prevention - check for common spam indicators
const spamIndicators = [
message.includes('http://'),
message.includes('https://'),
message.includes('www.'),
message.includes('bitcoin'),
message.includes('cryptocurrency'),
message.length < 10,
name.length < 2
]
// Verify captcha
const isValidCaptcha = verifyCaptcha(captchaId, captchaAnswer)
const spamScore = spamIndicators.filter(Boolean).length
if (spamScore >= 2) {
if (!isValidCaptcha) {
return NextResponse.json({
success: false,
error: 'Invalid captcha answer. Please try again.'
}, { status: 400 })
}
// Basic spam prevention - only check for obvious spam
// Allow URLs in messages since users may want to share links
const isSpam = (
(message.includes('bitcoin') || message.includes('cryptocurrency')) &&
(message.includes('http://') || message.includes('https://'))
)
if (isSpam) {
return NextResponse.json({
success: false,
error: 'Message flagged as potential spam'