import type { APIRoute } from 'astro'; import { submitEntry, checkRateLimit, recordSubmission, isDuplicateSubmission, moderateEntry, } from '../../../lib/guestbook'; import { hashIP } from '../../../lib/auth'; import { validateEntry, sanitizeText, stripHtml, validateWebsite, isLikelySpam } from '../../../lib/spam'; export const prerender = false; export const POST: APIRoute = async ({ request }) => { // Parse form data let formData: FormData; try { formData = await request.formData(); } catch { return redirect('/guestbook?error=invalid'); } const raw = { display_name: String(formData.get('display_name') ?? ''), message: String(formData.get('message') ?? ''), website: String(formData.get('website') ?? ''), consent: String(formData.get('consent') ?? ''), honeypot: String(formData.get('address') ?? ''), // hidden honeypot field named "address" }; // Validate const errors = validateEntry(raw); if (errors.some((e) => e.field === 'honeypot')) { // Silent reject for bots: appear successful return redirect('/guestbook?submitted=true'); } if (errors.length > 0) { const msg = encodeURIComponent(errors[0].message); return redirect(`/guestbook?error=${msg}`); } // Sanitize const display_name = stripHtml(sanitizeText(raw.display_name)); const message = stripHtml(sanitizeText(raw.message)); const website = validateWebsite(raw.website); // Rate limit by salted IP hash const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? request.headers.get('x-real-ip') ?? 'unknown'; const ipHash = hashIP(ip); if (!checkRateLimit(ipHash)) { return redirect('/guestbook?error=rate_limit'); } // Duplicate check if (isDuplicateSubmission(display_name, message)) { return redirect('/guestbook?submitted=true'); // silent dedupe } // Heuristic spam scoring: auto-mark as spam if score is high const spamEntry = isLikelySpam({ display_name, message, website }); recordSubmission(ipHash); const id = submitEntry({ display_name, message, website, ip_hash: ipHash }); // If auto-detected as spam, immediately mark it if (spamEntry) { moderateEntry(id, 'spam', 'auto-detected', 'system'); return redirect('/guestbook?submitted=true'); // still appear successful } return redirect('/guestbook?submitted=true'); }; function redirect(location: string): Response { return new Response(null, { status: 303, headers: { Location: location }, }); }