Anti-spam: add Cloudflare Turnstile (CAPTCHA) on registration form
- contact.html: ajout du widget Turnstile (site key: 0x4AAAAAADKDuc7Rmlb1svIL) - form-handler.js: blocage de la soumission si pas de token Turnstile valide - Worker: validation server-side du token via /turnstile/v0/siteverify avant chaque appel sendWelcomeNow → bloque les bots qui n'auraient pas passé le challenge côté client. Le secret Turnstile est en env var Cloudflare (TURNSTILE_SECRET). Limite humain : Turnstile détecte les bots avec très peu d'interaction côté utilisateur (mode 'Managed', le plus souvent invisible). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
313c870ea4
commit
a3a36df811
@ -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
|
||||
// =============================================================
|
||||
|
||||
10
contact.html
10
contact.html
@ -141,6 +141,9 @@
|
||||
<div class="form-error" id="error-cgv"></div>
|
||||
</div>
|
||||
|
||||
<!-- Cloudflare Turnstile (CAPTCHA invisible/léger pour bloquer les bots) -->
|
||||
<div class="cf-turnstile" data-sitekey="0x4AAAAAADKDuc7Rmlb1svIL" data-callback="onTurnstileSuccess" data-error-callback="onTurnstileError" style="margin-bottom: 16px;"></div>
|
||||
|
||||
<div id="formErrorGlobal" style="display:none; color: var(--red); margin-bottom: 16px; font-size: 0.9rem;"></div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn" style="width: 100%; justify-content: center;">
|
||||
@ -148,6 +151,13 @@
|
||||
<span id="submitText" data-i18n="contact.submitBtn">S'inscrire</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<script>
|
||||
window.turnstileToken = null;
|
||||
window.onTurnstileSuccess = function(token) { window.turnstileToken = token; };
|
||||
window.onTurnstileError = function() { window.turnstileToken = null; };
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<!-- COORDONNÉES -->
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user