diff --git a/cloudflare-worker/hubspot-proxy.js b/cloudflare-worker/hubspot-proxy.js index cb9e473..15bb0c4 100644 --- a/cloudflare-worker/hubspot-proxy.js +++ b/cloudflare-worker/hubspot-proxy.js @@ -107,8 +107,16 @@ export default { // Paris depuis env var). Appelé par form-handler.js après // soumission du formulaire. L'adresse n'apparaît jamais dans // le code JS public — elle vient des secrets Cloudflare. + // Anti-bot : on vérifie d'abord le token Cloudflare Turnstile. if (action === 'sendWelcomeNow') { if (!body.email) return jsonResponse({ error: 'email requis' }, 400); + + // Validation Turnstile (anti-bot) + const turnstileOk = await verifyTurnstile(env, body.turnstile_token, request); + if (!turnstileOk) { + return jsonResponse({ ok: false, error: 'Turnstile validation failed' }, 403); + } + try { await sendWelcomeEmail(env, { firstname : body.firstname || '', @@ -346,6 +354,39 @@ async function sendWelcomeEmail(env, params) { } } +// ============================================================= +// Cloudflare Turnstile : validation anti-bot +// ============================================================= +// Reçoit le token généré côté client (window.turnstileToken) et +// l'envoie à l'API Cloudflare avec le secret pour validation. +// Renvoie true uniquement si Cloudflare confirme que c'est un +// utilisateur humain. +async function verifyTurnstile(env, token, request) { + if (!token) return false; + if (!env.TURNSTILE_SECRET) { + // En dev / si pas configuré, on laisse passer (à durcir en prod) + console.warn('TURNSTILE_SECRET not set, skipping validation'); + return true; + } + const ip = request.headers.get('CF-Connecting-IP') || ''; + const formData = new FormData(); + formData.append('secret', env.TURNSTILE_SECRET); + formData.append('response', token); + if (ip) formData.append('remoteip', ip); + + try { + const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { + method: 'POST', + body: formData, + }); + const data = await res.json(); + return data.success === true; + } catch (err) { + console.warn('Turnstile verification error:', err); + return false; + } +} + // ============================================================= // Helpers // ============================================================= diff --git a/contact.html b/contact.html index 909981a..daedb2a 100644 --- a/contact.html +++ b/contact.html @@ -141,6 +141,9 @@
+ +
+ + + + diff --git a/js/form-handler.js b/js/form-handler.js index 91b854c..1d216c9 100644 --- a/js/form-handler.js +++ b/js/form-handler.js @@ -82,6 +82,17 @@ function setupContactForm(form) { e.preventDefault(); if (!validateForm(form)) return; + // ── VÉRIFICATION TURNSTILE (CAPTCHA anti-bot) ──────────────────────────── + if (!window.turnstileToken) { + const errEl = document.getElementById('formErrorGlobal'); + if (errEl) { + errEl.style.display = 'block'; + errEl.textContent = '🤖 Veuillez compléter le contrôle anti-robot ci-dessus avant d\'envoyer le formulaire.'; + } + return; + } + // ───────────────────────────────────────────────────────────────────────── + setLoading(true); const email = form.email.value.trim(); @@ -293,8 +304,9 @@ function showSuccess(refNumber, clientData) { // ── EMAIL DE BIENVENUE ──────────────────────────────────────────────────────── // Envoyé via le Cloudflare Worker pour que l'adresse Paris ne soit JAMAIS -// présente dans le JS public. Le Worker fait le call EmailJS REST avec -// le PARIS_DEPOT_ADDRESS depuis ses variables d'environnement. +// présente dans le JS public. Le Worker valide d'abord le token Turnstile +// (anti-bot) puis fait le call EmailJS REST avec le PARIS_DEPOT_ADDRESS +// depuis ses variables d'environnement. async function sendWelcomeEmail(data) { if (!WORKER_PROXY_URL) return; try { @@ -306,6 +318,7 @@ async function sendWelcomeEmail(data) { firstname: data.firstname, email: data.email, reference_client: data.reference_client, + turnstile_token: window.turnstileToken || '', }), }); } catch (err) {