From 7217f12bd255e71c314f4ed2f3d6c0868b0c07ef Mon Sep 17 00:00:00 2001 From: Serge RAKOTO HARRY-NAIVO Date: Sun, 10 May 2026 11:34:07 +0200 Subject: [PATCH] =?UTF-8?q?feat(api):=20migrate=20Worker=20mva-hubspot-pro?= =?UTF-8?q?xy=20=E2=86=92=20mva-api=20/leads/*=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le Cloudflare Worker hubspot-proxy était une solution temporaire car le panel admin AdminJS n'était pas encore disponible. Maintenant qu' AdminJS marche, on rapatrie tout le flow inscription côté MVA backend. ## Changements - `js/form-handler.js` - WORKER_PROXY_URL → API_BASE_URL = https://api.mva.mind4solutions.com - checkExistingContact : POST /leads/check-email (response shape : {exists, firstname, reference_client}) - setupContactForm : POST /leads/request-verification - sendWelcomeBackEmail : POST /leads/welcome-back - `js/confirmation.js` - WORKER_PROXY_URL → API_BASE_URL - POST /leads/verify-token (= au lieu de Worker action verifyToken) - Detection token expiré/invalide via code INVALID_OR_EXPIRED ## Aucun changement HTML Les forms HTML, IDs des éléments, validation côté client, gestion Turnstile sont tous inchangés. Seules les URLs API changent. ## Côté backend (PR #44 mva-prestige-v2) Les routes mva-api /leads/* sont déployées séparément avec : - Validation Zod + Turnstile + rate limit - DB Postgres (table leads + leads_pending) remplace HubSpot Contacts + KV - Resend pour les emails (= unification écosystème M4S) - AdminJS Resource leads pour Mélissa CRUD ## Cutover Cette PR doit être merged + déployée APRÈS la migration backend (PR #44) + après le run du script migrate-hubspot-to-postgres.js (= les 4 contacts HubSpot existants en DB). --- js/confirmation.js | 34 ++++++++++++++---------- js/form-handler.js | 64 ++++++++++++++++++++++------------------------ 2 files changed, 51 insertions(+), 47 deletions(-) diff --git a/js/confirmation.js b/js/confirmation.js index 20ddb08..66dfe32 100644 --- a/js/confirmation.js +++ b/js/confirmation.js @@ -1,23 +1,27 @@ // ============================================================ // MVA Global Fret — Page de confirmation post-validation email // ============================================================ -// Cette page est la cible du lien dans l'email de validation -// (envoyé par Brevo après soumission du formulaire). +// Cette page est la cible du lien dans l'email de validation envoyé +// par mva-api (= Resend) lors d'une inscription via contact.html. // -// URL : https://mva-global-fret.github.io/site-mva-global-fret/confirmation.html?token=XXX +// URL : https://mva-globalfret.com/confirmation.html?token=XXX // // Étapes : // 1. Lire le token depuis l'URL -// 2. POST au Worker avec action 'verifyToken' -// 3. Worker valide le token, envoie le welcome email (avec ref + -// adresse Paris) via Brevo, puis renvoie OK -// 4. Page affiche "Inscription confirmée !" +// 2. POST mva-api /leads/verify-token avec { token } +// 3. mva-api INSERT le lead en DB, génère la ref MVA-NNN, envoie le +// welcome email (= ref + adresse Paris) via Resend, puis renvoie +// { ok: true, firstname, reference_client } +// 4. Page affiche "Inscription confirmée !" + la ref // -// Si le token est invalide / expiré : affichage d'un message d'erreur -// avec invitation à contacter le support. +// Si le token est invalide / expiré / déjà consommé : affichage d'un +// message d'erreur avec invitation à contacter le support. +// +// Migration 2026-05-10 : remplace l'ancien Cloudflare Worker +// `mva-hubspot-proxy.sergemind4s.workers.dev` (= décommissionné). // ============================================================ -const WORKER_PROXY_URL = 'https://mva-hubspot-proxy.sergemind4s.workers.dev'; +const API_BASE_URL = 'https://api.mva.mind4solutions.com'; document.addEventListener('DOMContentLoaded', async () => { const token = new URLSearchParams(window.location.search).get('token'); @@ -28,18 +32,20 @@ document.addEventListener('DOMContentLoaded', async () => { } try { - const res = await fetch(WORKER_PROXY_URL, { + const res = await fetch(`${API_BASE_URL}/leads/verify-token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action: 'verifyToken', token }), + body: JSON.stringify({ token }), }); const data = await res.json(); if (data.ok) { showSuccess(data.reference_client || null); } else { - // Token expiré, déjà utilisé, ou inconnu - showError(data.error === 'Token invalide ou expiré' + // Token invalide, expiré, ou inconnu + const isInvalid = data.code === 'INVALID_OR_EXPIRED' + || data.message === 'Token invalide ou expiré'; + showError(isInvalid ? 'Ce lien de confirmation a expiré ou a déjà été utilisé.' : 'Une erreur est survenue lors de la confirmation.'); } diff --git a/js/form-handler.js b/js/form-handler.js index 43bf992..a6b3cad 100644 --- a/js/form-handler.js +++ b/js/form-handler.js @@ -3,23 +3,21 @@ // ============================================ // Frontend logic for contact.html (= inscription form): // - validate inputs + Cloudflare Turnstile token -// - call Cloudflare Worker mva-hubspot-proxy for HubSpot dedup + -// sending verification email (= action requestVerification) or -// "Ravis de vous revoir" email for returning customers (= -// action sendWelcomeBack) -// - reset Turnstile widget after each Worker call (= tokens are +// - call mva-api /leads/* routes for dedup check + double opt-in flow +// (= verification email + welcome / welcome-back emails via Resend) +// - reset Turnstile widget after each API call (= tokens are // single-use server-side; without reset, a re-submit silently // 403s from Cloudflare's siteverify endpoint) // -// All HubSpot/Resend transactions go through the Worker. No direct -// EmailJS / Formspree / HubSpot Forms API calls from the browser. +// Migration 2026-05-10 : remplace l'ancien Cloudflare Worker +// `mva-hubspot-proxy.sergemind4s.workers.dev` (= décommissionné) par +// les routes mva-api Fastify. La DB Postgres remplace HubSpot Contacts. // ============================================ -// ── PROXY CLOUDFLARE WORKER ────────────────────────────────────── -// Worker URL (= deployed via wrangler from cloudflare-worker/). -// CORS Access-Control-Allow-Origin: * so the browser can call it -// directly. -const WORKER_PROXY_URL = 'https://mva-hubspot-proxy.sergemind4s.workers.dev'; +// ── MVA API BASE URL ───────────────────────────────────────────── +// Routes leads servies par mva-api derrière Caddy. CORS strict : +// le serveur whitelist explicitement https://mva-globalfret.com. +const API_BASE_URL = 'https://api.mva.mind4solutions.com'; document.addEventListener('DOMContentLoaded', () => { const form = document.getElementById('contactForm'); @@ -39,20 +37,24 @@ function resetTurnstile() { } } -// Vérifie si l'email existe déjà dans HubSpot via le proxy Worker. -// Retourne les propriétés du contact existant, ou null si nouveau -// client / Worker indisponible. +// Vérifie si l'email existe déjà dans la table leads via mva-api. +// Retourne les propriétés du lead existant, ou null si nouveau +// client / API indisponible. async function checkExistingContact(email) { - if (!WORKER_PROXY_URL) return null; try { - const res = await fetch(WORKER_PROXY_URL, { + const res = await fetch(`${API_BASE_URL}/leads/check-email`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email.toLowerCase().trim() }), }); if (!res.ok) return null; const data = await res.json(); - return data.total > 0 ? data.results[0].properties : null; + if (!data.exists) return null; + // Forme attendue par showAlreadyRegistered : { firstname, reference_client } + return { + firstname: data.firstname || '', + reference_client: data.reference_client || '', + }; } catch { return null; } @@ -95,18 +97,17 @@ function setupContactForm(form) { address: form.address.value.trim(), }; - // ── ENVOI VERS LE WORKER ───────────────────────────────────── - // Le Worker stocke les données en KV (24h) et envoie un email de - // validation via Resend. Le contact n'est créé dans HubSpot QUE + // ── ENVOI VERS MVA-API ──────────────────────────────────────── + // L'API stocke les données en `leads_pending` (24h TTL) et envoie un + // email de validation via Resend. Le lead n'est INSERT en `leads` QUE // quand l'utilisateur clique sur le lien de confirmation - // (anti-pollution du CRM). + // (anti-pollution DB + anti-bot complémentaire à Turnstile). let ok = false; try { - const res = await fetch(WORKER_PROXY_URL, { + const res = await fetch(`${API_BASE_URL}/leads/request-verification`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - action: 'requestVerification', ...data, turnstile_token: window.turnstileToken || '', }), @@ -221,29 +222,26 @@ function showSuccess(_refNumber, _clientData) { } // ── EMAIL "RAVIS DE VOUS REVOIR" (client déjà inscrit) ─────────── -// Rappelle au client son numéro de référence existant — n'écrit -// RIEN dans HubSpot. Passe par le Cloudflare Worker (action: -// sendWelcomeBack) qui délègue à Resend. Anti-bot via Turnstile : -// transmet le token déjà validé au moment du submit du formulaire. +// Rappelle au client son numéro de référence existant — zéro write DB. +// Passe par mva-api /leads/welcome-back qui délègue à Resend. +// Anti-bot via Turnstile : transmet le token déjà validé au moment du +// submit du formulaire. async function sendWelcomeBackEmail(contact) { - if (!WORKER_PROXY_URL) return; if (!contact || !contact.email) return; if (!window.turnstileToken) return; try { - await fetch(WORKER_PROXY_URL, { + await fetch(`${API_BASE_URL}/leads/welcome-back`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - action : 'sendWelcomeBack', email : contact.email, - firstname : contact.firstname || '', turnstile_token : window.turnstileToken, }), }); } catch (err) { // Erreur réseau : on n'interrompt pas l'UX (le client voit // déjà sa référence dans le UI). - console.warn('Worker sendWelcomeBack failed:', err); + console.warn('welcome-back failed:', err); } } -- 2.45.2