mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
Form Update
This commit is contained in:
@@ -5,9 +5,10 @@
|
||||
* 1. Reject non-POST.
|
||||
* 2. Validate JSON body with Zod (name, email, message; honeypot must be empty).
|
||||
* 3. Rate-limit per IP (in-memory windowed counter).
|
||||
* 4. Deliver: SMTP via nodemailer if configured, else POST to a webhook if
|
||||
* 4. Verify Cloudflare Turnstile token (if TURNSTILE_SECRET_KEY is set).
|
||||
* 5. Deliver: SMTP via nodemailer if configured, else POST to a webhook if
|
||||
* configured, else return 503 so delivery can be wired later.
|
||||
* 5. Always return JSON { ok } or { error }.
|
||||
* 6. Always return JSON { ok } or { error }.
|
||||
*
|
||||
* Secrets come from env (see .env.example). Nothing secret ships to the client.
|
||||
* This runs server-side only via the @astrojs/node adapter.
|
||||
@@ -22,6 +23,9 @@ const schema = z.object({
|
||||
message: z.string().min(10).max(5000),
|
||||
// Honeypot: must be absent/empty. Bots tend to fill every field.
|
||||
company: z.string().max(0).optional().or(z.literal('')),
|
||||
// Turnstile token. Required when TURNSTILE_SECRET_KEY is set; ignored
|
||||
// otherwise (dev). Validated against Cloudflare's siteverify endpoint below.
|
||||
turnstileToken: z.string().optional(),
|
||||
});
|
||||
|
||||
// --- In-memory rate limiter (per worker) ------------------------------------
|
||||
@@ -57,6 +61,36 @@ function clientIp(request: Request): string {
|
||||
return request.headers.get('x-real-ip') ?? 'unknown';
|
||||
}
|
||||
|
||||
// --- Turnstile verification -------------------------------------------------
|
||||
// Skipped entirely when TURNSTILE_SECRET_KEY is unset (local dev). When set,
|
||||
// the token from the client is POSTed to Cloudflare's siteverify endpoint
|
||||
// along with the visitor's IP for fraud scoring. Only valid tokens proceed.
|
||||
async function verifyTurnstile(token: string | undefined, ip: string): Promise<boolean> {
|
||||
const secret = process.env.TURNSTILE_SECRET_KEY;
|
||||
if (!secret) return true; // Turnstile not configured -> skip.
|
||||
|
||||
if (!token) return false;
|
||||
|
||||
try {
|
||||
const form = new URLSearchParams();
|
||||
form.set('secret', secret);
|
||||
form.set('response', token);
|
||||
if (ip && ip !== 'unknown') form.set('remoteip', ip);
|
||||
|
||||
const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
});
|
||||
const data = (await res.json()) as { success: boolean };
|
||||
return Boolean(data.success);
|
||||
} catch (err) {
|
||||
// Network error talking to Cloudflare. Fail closed: reject the submission
|
||||
// rather than letting a bot through on a transient outage.
|
||||
console.error('[contact] turnstile verification failed:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Route handler ----------------------------------------------------------
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
if (request.method !== 'POST') {
|
||||
@@ -74,7 +108,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
if (!parsed.success) {
|
||||
return json({ error: 'Please complete all fields correctly.', issues: parsed.error.issues }, 400);
|
||||
}
|
||||
const { name, email, message } = parsed.data;
|
||||
const { name, email, message, turnstileToken } = parsed.data;
|
||||
|
||||
// Rate limit.
|
||||
const ip = clientIp(request);
|
||||
@@ -82,6 +116,13 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return json({ error: 'Too many submissions. Please try again later.' }, 429);
|
||||
}
|
||||
|
||||
// Turnstile. Skipped when no secret is configured (dev). Fails closed on
|
||||
// network errors so a Cloudflare outage can't open the door to bots.
|
||||
const turnstileOk = await verifyTurnstile(turnstileToken, ip);
|
||||
if (!turnstileOk) {
|
||||
return json({ error: 'Bot check failed. Please refresh and try again.' }, 400);
|
||||
}
|
||||
|
||||
// Deliver.
|
||||
try {
|
||||
const delivered = await deliver({ name, email, message });
|
||||
|
||||
Reference in New Issue
Block a user