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
|
// Paris depuis env var). Appelé par form-handler.js après
|
||||||
// soumission du formulaire. L'adresse n'apparaît jamais dans
|
// soumission du formulaire. L'adresse n'apparaît jamais dans
|
||||||
// le code JS public — elle vient des secrets Cloudflare.
|
// le code JS public — elle vient des secrets Cloudflare.
|
||||||
|
// Anti-bot : on vérifie d'abord le token Cloudflare Turnstile.
|
||||||
if (action === 'sendWelcomeNow') {
|
if (action === 'sendWelcomeNow') {
|
||||||
if (!body.email) return jsonResponse({ error: 'email requis' }, 400);
|
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 {
|
try {
|
||||||
await sendWelcomeEmail(env, {
|
await sendWelcomeEmail(env, {
|
||||||
firstname : body.firstname || '',
|
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
|
// Helpers
|
||||||
// =============================================================
|
// =============================================================
|
||||||
|
|||||||
10
contact.html
10
contact.html
@ -141,6 +141,9 @@
|
|||||||
<div class="form-error" id="error-cgv"></div>
|
<div class="form-error" id="error-cgv"></div>
|
||||||
</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>
|
<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;">
|
<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>
|
<span id="submitText" data-i18n="contact.submitBtn">S'inscrire</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- COORDONNÉES -->
|
<!-- COORDONNÉES -->
|
||||||
|
|||||||
@ -82,6 +82,17 @@ function setupContactForm(form) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!validateForm(form)) return;
|
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);
|
setLoading(true);
|
||||||
|
|
||||||
const email = form.email.value.trim();
|
const email = form.email.value.trim();
|
||||||
@ -293,8 +304,9 @@ function showSuccess(refNumber, clientData) {
|
|||||||
|
|
||||||
// ── EMAIL DE BIENVENUE ────────────────────────────────────────────────────────
|
// ── EMAIL DE BIENVENUE ────────────────────────────────────────────────────────
|
||||||
// Envoyé via le Cloudflare Worker pour que l'adresse Paris ne soit JAMAIS
|
// 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
|
// présente dans le JS public. Le Worker valide d'abord le token Turnstile
|
||||||
// le PARIS_DEPOT_ADDRESS depuis ses variables d'environnement.
|
// (anti-bot) puis fait le call EmailJS REST avec le PARIS_DEPOT_ADDRESS
|
||||||
|
// depuis ses variables d'environnement.
|
||||||
async function sendWelcomeEmail(data) {
|
async function sendWelcomeEmail(data) {
|
||||||
if (!WORKER_PROXY_URL) return;
|
if (!WORKER_PROXY_URL) return;
|
||||||
try {
|
try {
|
||||||
@ -306,6 +318,7 @@ async function sendWelcomeEmail(data) {
|
|||||||
firstname: data.firstname,
|
firstname: data.firstname,
|
||||||
email: data.email,
|
email: data.email,
|
||||||
reference_client: data.reference_client,
|
reference_client: data.reference_client,
|
||||||
|
turnstile_token: window.turnstileToken || '',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user