From 32c7d656980483814f5132be8510a55fe4a41153 Mon Sep 17 00:00:00 2001 From: Serge RAKOTO HARRY-NAIVO Date: Sat, 16 May 2026 16:39:22 +0200 Subject: [PATCH] chore: remove decommissioned cloudflare-worker Le worker hubspot-proxy.js a ete decommissione le 2026-05-10 : migration HubSpot CRM -> mva-api Fastify + Postgres + Resend (= mention explicite dans js/form-handler.js). Le dossier cloudflare-worker/ (DEPLOIEMENT.md + hubspot-proxy.js + wrangler.toml) n'est plus utilise par le frontend mais traine dans le repo. Cleanup. Refs: - js/form-handler.js commentaire 'Migration 2026-05-10 : remplace l ancien Cloudflare Worker mva-hubspot-proxy.sergemind4s.workers.dev (= decommissionne) par les routes mva-api Fastify. La DB Postgres remplace HubSpot Contacts.' - Audit hygiene M4S 2026-05-16 --- cloudflare-worker/.gitignore | 1 - cloudflare-worker/DEPLOIEMENT.md | 149 ------- cloudflare-worker/hubspot-proxy.js | 675 ----------------------------- cloudflare-worker/wrangler.toml | 19 - 4 files changed, 844 deletions(-) delete mode 100644 cloudflare-worker/.gitignore delete mode 100644 cloudflare-worker/DEPLOIEMENT.md delete mode 100644 cloudflare-worker/hubspot-proxy.js delete mode 100644 cloudflare-worker/wrangler.toml diff --git a/cloudflare-worker/.gitignore b/cloudflare-worker/.gitignore deleted file mode 100644 index b75a0fa..0000000 --- a/cloudflare-worker/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.wrangler/ diff --git a/cloudflare-worker/DEPLOIEMENT.md b/cloudflare-worker/DEPLOIEMENT.md deleted file mode 100644 index 8c031d8..0000000 --- a/cloudflare-worker/DEPLOIEMENT.md +++ /dev/null @@ -1,149 +0,0 @@ -# Déploiement Cloudflare Worker — Double opt-in MVA via Resend - -Ce Worker gère le flow double opt-in du formulaire de contact : - -1. **`requestVerification`** — génère un token, stocke les données du formulaire en KV, envoie un email de validation via Resend. -2. **`verifyToken`** — appelé quand le client clique sur le lien dans l'email. Crée le contact dans HubSpot (avec une référence à la volée) et envoie le welcome email avec la référence + l'adresse du dépôt Paris. - -Le Worker est aussi un proxy HubSpot pour la vérification de doublon par email et la génération du prochain numéro de référence séquentiel. - -L'email de bienvenue contient le **numéro de référence client** ET **l'adresse du dépôt à Paris**. Ces infos ne sont **jamais** envoyées avant validation de l'email — protection contre les bots et les expéditions de cartons vides. - ---- - -## Étapes de déploiement (Phase D du plan WordPress → static) - -### 1. Mettre à jour le code du Worker - -Préférer `wrangler` : - -```bash -cd cloudflare-worker -wrangler deploy -``` - -Ou via le dashboard Cloudflare : - -1. Aller sur https://dash.cloudflare.com/ -2. Workers & Pages → cliquer sur **`mva-hubspot-proxy`** -3. Onglet **Modifier le code** (ou *Quick edit*) -4. Tout sélectionner et remplacer par le contenu de `cloudflare-worker/hubspot-proxy.js` -5. Cliquer **Déployer / Save and deploy** - -### 2. Secrets - -Dans **Paramètres → Variables et secrets** (ou via CLI : `wrangler secret put `) : - -| Nom | Valeur | -|-----|--------| -| `HUBSPOT_TOKEN` | `pat-eu1-...` (scope read+write contacts) | -| `RESEND_API_KEY` | `re_...` (compte Resend partagé avec m4s-auth) | -| `RESEND_FROM_EMAIL` | adresse expéditrice (domaine vérifié chez Resend) | -| `RESEND_FROM_NAME` | nom affiché à l'expéditeur (ex: `MVA Global Fret`) | -| `PARIS_DEPOT_ADDRESS` | **Ton adresse exacte à Paris** (rue, code postal, ville, étage, nom à indiquer sur le carton…) | -| `TURNSTILE_SECRET` | secret Cloudflare Turnstile (anti-bot) | -| `SITE_URL` | base URL du site (ex: `https://mva-globalfret.com`) | - -> `PARIS_DEPOT_ADDRESS` est l'info la plus sensible — c'est l'adresse à protéger. Elle ne quitte jamais Cloudflare/Resend et n'arrive au client que dans le mail de bienvenue, qui n'est envoyé qu'aux contacts qui ont confirmé leur email. - -### 3. Stockage KV (idempotence — empêche de spammer le welcome) - -Dans **Paramètres → Stockage et bases de données → Bindings KV** : - -1. Cliquer **Ajouter un binding** -2. Variable name : **`WELCOME_KV`** -3. Namespace : **Créer** un nouveau namespace nommé `mva-welcome-tracker` -4. Sauvegarder - -Si déployé via `wrangler` : `wrangler.toml` contient déjà l'ID du namespace KV (`c02656ba22064923ab1c6db06b0f4a56` sur le compte CF `sergemind4s@gmail.com`). Pour un autre compte, recréer le namespace via `wrangler kv namespace create WELCOME_KV` puis remplacer l'ID dans `wrangler.toml`. - -### 4. Vérifier le scope HubSpot - -Le token HubSpot doit avoir : - -- `crm.objects.contacts.read` -- `crm.objects.contacts.write` -- `crm.lists.read` - -Si tu obtiens des erreurs 403, regénère le token sur https://app-eu1.hubspot.com/private-apps/148163754/ - -### 5. Resend — vérifier le domaine expéditeur - -Le compte Resend (partagé avec m4s-auth) doit avoir le domaine de `RESEND_FROM_EMAIL` vérifié (DNS records SPF + DKIM). Voir https://resend.com/domains. - ---- - -## Test manuel - -Une fois tout déployé, tu peux tester le flow `requestVerification` (token de test, ne pas réutiliser en prod) : - -```bash -curl -X POST https://mva-hubspot-proxy..workers.dev \ - -H "Content-Type: application/json" \ - -d '{ - "action": "requestVerification", - "firstname": "Test", - "lastname": "User", - "email": "test@example.com", - "phone": "+33000000000", - "address": "Test address", - "turnstile_token": "" - }' -``` - -Réponse attendue : -```json -{ "ok": true } -``` - -Le client reçoit alors un email de validation. En cliquant sur le lien, il déclenche `verifyToken`, qui crée le contact dans HubSpot et envoie le welcome email avec la référence + l'adresse Paris. - ---- - -## Logs en production - -Cloudflare → ton Worker → **Logs (en temps réel)** : tu verras chaque exécution du Worker. - ---- - -## En cas de problème - -| Symptôme | Cause probable | Fix | -|---|---|---| -| `Resend 401: API key invalid` | Mauvaise valeur dans `RESEND_API_KEY` | Re-vérifier la clé sur https://resend.com/api-keys | -| `Resend 422: domain is not verified` | Domaine de `RESEND_FROM_EMAIL` pas vérifié | Vérifier le domaine sur https://resend.com/domains | -| `HubSpot 403` | Token n'a pas le scope write | Regénérer token avec scope `contacts.write` | -| `HubSpot 401` | Token invalide / expiré | Regénérer un Private App token | -| `KV not bound` | Binding `WELCOME_KV` pas créé | Vérifier *Storage & Databases → KV bindings* | -| `Turnstile validation failed` | Secret côté Worker ne match pas la sitekey côté front | Re-vérifier `TURNSTILE_SECRET` et la sitekey HTML | - ---- - -## Architecture - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ 1. User remplit le formulaire sur contact.html │ -│ ↓ │ -│ 2. form-handler.js → POST action: requestVerification │ -│ ↓ │ -│ 3. Worker valide Turnstile, génère un token, stocke les │ -│ données du formulaire en KV (TTL 24h), envoie l'email │ -│ de validation via Resend │ -│ ↓ │ -│ ─────── User clique sur « Confirmer » ─────── │ -│ ↓ │ -│ 4. confirmation.html → POST action: verifyToken │ -│ ↓ │ -│ 5. Worker récupère les données du KV, génère la prochaine │ -│ référence client, soumet via HubSpot Forms API (= contact │ -│ créé dans le CRM), envoie le welcome email via Resend │ -│ (avec la référence + adresse Paris), marque le token │ -│ consommé en KV (TTL 7j pour idempotence). │ -│ ↓ │ -│ 6. Le client reçoit son welcome email avec sa référence ET │ -│ l'adresse de dépôt à Paris. │ -└──────────────────────────────────────────────────────────────────┘ -``` - -**Conclusion** : il est impossible pour un bot de récupérer la référence client ou l'adresse Paris sans avoir au préalable un email valide ET cliqué sur le lien de confirmation. Anti-spam blindé. diff --git a/cloudflare-worker/hubspot-proxy.js b/cloudflare-worker/hubspot-proxy.js deleted file mode 100644 index d8508bd..0000000 --- a/cloudflare-worker/hubspot-proxy.js +++ /dev/null @@ -1,675 +0,0 @@ -// ============================================================ -// MVA Global Fret — Cloudflare Worker : Proxy HubSpot + double opt-in via Resend -// ============================================================ -// Ce Worker gère le formulaire de contact via un flow double opt-in : -// -// 1) requestVerification : génère un token, stocke les données du formulaire en KV, -// envoie un email de validation (lien de confirmation) via Resend. -// -// 2) verifyToken : appelé quand le client clique sur le lien de confirmation. -// Crée le contact dans HubSpot (avec une référence générée à la volée), -// puis envoie le welcome email avec la référence + l'adresse du dépôt Paris. -// Idempotent : un 2ème clic ne re-crée pas de contact ni ne renvoie d'email. -// -// La référence client + l'adresse du dépôt à Paris ne fuitent jamais avant -// validation de l'email — protection anti-bot et anti-cartons-vides. -// -// ============================================================ -// DÉPLOIEMENT (Phase D du plan WordPress → static) -// ============================================================ -// -// Voir cloudflare-worker/DEPLOIEMENT.md pour la procédure complète. -// -// Secrets requis (`wrangler secret put `) : -// • HUBSPOT_TOKEN = pat-eu1-... (read+write contacts) -// • RESEND_API_KEY = re_... (compte Resend partagé avec m4s-auth) -// • RESEND_FROM_EMAIL = adresse expéditrice (domaine vérifié chez Resend) -// • RESEND_FROM_NAME = nom affiché à l'expéditeur -// • PARIS_DEPOT_ADDRESS = adresse complète du dépôt Paris -// • TURNSTILE_SECRET = secret Cloudflare Turnstile (anti-bot) -// • SITE_URL = base URL du site (ex: "https://mva-globalfret.com") -// -// Bindings KV requis : -// • WELCOME_KV → namespace `mva-welcome-tracker` (idempotence verifyToken) -// -// ============================================================ - -const HUBSPOT_API = 'https://api.hubapi.com'; - -const corsHeaders = { - 'Access-Control-Allow-Origin' : '*', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', -}; - -export default { - // ----------------------------------------------------------- - // 1) Handler navigateur (POST depuis le formulaire / page de confirmation) - // ----------------------------------------------------------- - async fetch(request, env) { - - if (request.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - if (request.method !== 'POST') { - return new Response('Method Not Allowed', { status: 405 }); - } - - const token = env.HUBSPOT_TOKEN; - if (!token) { - return jsonResponse({ error: 'HUBSPOT_TOKEN env var not set' }, 500); - } - - try { - const body = await request.json(); - const { email, action } = body; - - // ── action: nextRef ───────────────────────────────────── - if (action === 'nextRef') { - return jsonResponse({ nextRef: await getNextRef(token) }); - } - - // ── action: requestVerification ────────────────────────── - // Génère un token unique, stocke TOUTES les données du formulaire en KV, - // et envoie un email de validation via Resend. Le contact n'est créé - // dans HubSpot QU'APRÈS clic sur le lien de confirmation (anti-spam : - // les inscriptions non vérifiées ne polluent pas le CRM). - // Anti-bot : Turnstile vérifié d'abord. - if (action === 'requestVerification') { - if (!body.email) return jsonResponse({ error: 'email requis' }, 400); - - const turnstileOk = await verifyTurnstile(env, body.turnstile_token, request); - if (!turnstileOk) { - return jsonResponse({ ok: false, error: 'Turnstile validation failed' }, 403); - } - - try { - const verToken = crypto.randomUUID().replace(/-/g, ''); - const tokenData = { - firstname : body.firstname || '', - lastname : body.lastname || '', - phone : body.phone || '', - email : body.email.toLowerCase().trim(), - address : body.address || '', - createdAt : new Date().toISOString(), - }; - - if (!env.WELCOME_KV) { - return jsonResponse({ ok: false, error: 'KV not bound' }, 500); - } - // Token valide 24h - await env.WELCOME_KV.put(`verify:${verToken}`, JSON.stringify(tokenData), { - expirationTtl: 60 * 60 * 24, - }); - - await sendVerificationEmail(env, tokenData, verToken); - return jsonResponse({ ok: true }); - } catch (err) { - return jsonResponse({ ok: false, error: err.message }, 500); - } - } - - // ── action: verifyToken ────────────────────────────────── - // Appelé par confirmation.html quand l'utilisateur clique sur - // le lien dans l'email de validation. C'est ICI que le contact - // est CRÉÉ dans HubSpot (avec une référence générée à la volée), - // puis le welcome email est envoyé (ref + adresse Paris). - // Idempotent : un 2ème clic ne re-crée pas de contact. - if (action === 'verifyToken') { - if (!body.token) return jsonResponse({ error: 'token requis' }, 400); - if (!env.WELCOME_KV) return jsonResponse({ ok: false, error: 'KV not bound' }, 500); - - const key = `verify:${body.token}`; - const raw = await env.WELCOME_KV.get(key); - if (!raw) { - return jsonResponse({ ok: false, error: 'Token invalide ou expiré' }, 404); - } - const tokenData = JSON.parse(raw); - - // Idempotence : si déjà consommé, retourne le résultat précédent - // sans recréer le contact ni renvoyer d'email. - if (tokenData.used) { - return jsonResponse({ - ok: true, - firstname : tokenData.firstname, - reference_client : tokenData.reference_client || '', - }); - } - - try { - // 1) Récupère la ref existante si le contact est déjà dans HubSpot - // (réinscription après suppression d'un test, ou création via - // l'ancien flow Forms API). Sinon génère la prochaine ref. - let refNumber; - try { - const existing = await searchContactByEmail(token, tokenData.email); - const existingResult = (existing.results || [])[0]; - const existingRef = existingResult?.properties?.reference_client; - refNumber = existingRef || await getNextRef(token); - } catch (_) { - // Si la search échoue (scope manquant, etc.), fallback : génère - // une nouvelle ref. Le Forms API gérera la dédup côté HubSpot. - refNumber = await getNextRef(token); - } - - // 2) Création directe via CRM API (= more deterministic que Forms API - // qui peut accepter une submission sans réellement créer le contact - // à cause des filtres anti-spam ou de la config du Form HubSpot). - // Requires scope crm.objects.contacts.write. - // En cas de 409 (contact déjà existant), fallback sur PATCH par ID - // pour update les propriétés (= notamment reference_client). - const crmRes = await fetch( - `${HUBSPOT_API}/crm/v3/objects/contacts`, - { - method: 'POST', - headers: { - 'Content-Type' : 'application/json', - 'Authorization': `Bearer ${token}`, - }, - body: JSON.stringify({ - properties: { - firstname : tokenData.firstname || '', - lastname : tokenData.lastname || '', - phone : tokenData.phone || '', - email : tokenData.email, - address : tokenData.address || '', - reference_client : refNumber, - }, - }), - } - ); - if (crmRes.status === 409) { - // Contact existe déjà — update via PATCH par ID - const search = await searchContactByEmail(token, tokenData.email); - const existing = (search.results || [])[0]; - if (existing?.id) { - const patchRes = await fetch( - `${HUBSPOT_API}/crm/v3/objects/contacts/${existing.id}`, - { - method: 'PATCH', - headers: { - 'Content-Type' : 'application/json', - 'Authorization': `Bearer ${token}`, - }, - body: JSON.stringify({ - properties: { - firstname : tokenData.firstname || existing.properties?.firstname || '', - lastname : tokenData.lastname || existing.properties?.lastname || '', - phone : tokenData.phone || existing.properties?.phone || '', - address : tokenData.address || '', - reference_client : refNumber, - }, - }), - } - ); - if (!patchRes.ok) { - const errTxt = await patchRes.text(); - throw new Error(`HubSpot CRM patch failed ${patchRes.status}: ${errTxt.slice(0, 200)}`); - } - } - } else if (!crmRes.ok) { - const errTxt = await crmRes.text(); - throw new Error(`HubSpot CRM create failed ${crmRes.status}: ${errTxt.slice(0, 200)}`); - } - - // 3) Envoie le welcome email avec ref + adresse Paris - const welcomeContact = { ...tokenData, reference_client: refNumber }; - await sendWelcomeViaResend(env, welcomeContact); - - // 4) Marque le token consommé (gardé 7j pour idempotence) - await env.WELCOME_KV.put(key, JSON.stringify({ - ...tokenData, - used : true, - usedAt : new Date().toISOString(), - reference_client : refNumber, - }), { - expirationTtl: 60 * 60 * 24 * 7, - }); - - return jsonResponse({ - ok: true, - firstname : tokenData.firstname, - reference_client : refNumber, - }); - } catch (err) { - return jsonResponse({ ok: false, error: err.message }, 500); - } - } - - // ── action: sendWelcomeBack ───────────────────────────── - // Envoie un email "Vous êtes déjà inscrit" au client qui tente - // une ré-inscription. Idempotent côté HubSpot (= aucune création - // ni update de contact). Anti-bot via Turnstile + sanity check - // que l'email existe vraiment dans HubSpot avant d'envoyer. - if (action === 'sendWelcomeBack') { - if (!body.email) return jsonResponse({ error: 'email requis' }, 400); - - const turnstileOk = await verifyTurnstile(env, body.turnstile_token, request); - if (!turnstileOk) { - return jsonResponse({ ok: false, error: 'Turnstile validation failed' }, 403); - } - - try { - // Vérification : le contact existe bien (= prevent spam vers - // emails inconnus en passant un faux turnstile) - const search = await searchContactByEmail(token, body.email); - const existing = (search.results || [])[0]; - if (!existing) { - return jsonResponse({ ok: false, error: 'Contact not found' }, 404); - } - - await sendWelcomeBackViaResend(env, { - firstname : body.firstname || existing.properties?.firstname || '', - email : body.email, - reference_client : existing.properties?.reference_client || '', - }); - return jsonResponse({ ok: true }); - } catch (err) { - return jsonResponse({ ok: false, error: err.message }, 500); - } - } - - // ── action par défaut : vérification doublon par email ── - if (!email || typeof email !== 'string') { - return jsonResponse({ error: 'Email requis' }, 400); - } - - const data = await searchContactByEmail(token, email); - return jsonResponse(data); - - } catch (err) { - return jsonResponse({ error: err.message }, 500); - } - }, -}; - -// ============================================================= -// HubSpot : recherches & lectures -// ============================================================= - -async function searchContactByEmail(token, email) { - const res = await fetch(`${HUBSPOT_API}/crm/v3/objects/contacts/search`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - body: JSON.stringify({ - filterGroups: [{ - filters: [{ propertyName: 'email', operator: 'EQ', value: email.toLowerCase().trim() }], - }], - properties: ['firstname', 'lastname', 'email', 'reference_client'], - }), - }); - - if (!res.ok) { - throw new Error(`HubSpot lookup failed: ${res.status}`); - } - return res.json(); -} - -async function getNextRef(token) { - // Paginate through ALL HubSpot contacts with `reference_client` property to - // find the true numeric maximum. Previous version used `limit: 100` without - // pagination — produced collisions once the contact count exceeded 100 - // because HubSpot search results don't guarantee ordering by ref. With - // pagination, we walk the full set: 100 per page × N pages until no more. - // For ~1000 contacts = 10 API calls. Acceptable cost given that this runs - // once per signup confirmation (= rare path). - let maxNum = 0; - let after; // undefined on first iteration - do { - const body = { - filterGroups: [{ - filters: [{ propertyName: 'reference_client', operator: 'HAS_PROPERTY' }], - }], - properties: ['reference_client'], - limit: 100, - }; - if (after) body.after = after; - - const res = await fetch(`${HUBSPOT_API}/crm/v3/objects/contacts/search`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - body: JSON.stringify(body), - }); - if (!res.ok) { - throw new Error(`HubSpot search failed: ${res.status}`); - } - const data = await res.json(); - (data.results || []).forEach(c => { - const m = (c.properties?.reference_client || '').match(/^MVA-(\d+)$/); - if (m) { - const n = parseInt(m[1], 10); - if (n > maxNum) maxNum = n; - } - }); - after = data.paging?.next?.after; - } while (after); - return 'MVA-' + String(maxNum + 1).padStart(3, '0'); -} - -// ============================================================= -// Resend : envoi d'emails transactionnels (verification + welcome) -// ============================================================= -// Aligné avec m4s-auth (Phase 2.1) qui utilise déjà Resend en production. -// Le compte Resend (et le domaine vérifié) sont partagés entre m4s-auth et -// ce Worker — un seul fournisseur SMTP pour tout Mind4Solutions. -// -// Setup requis (`wrangler secret put `) : -// - env.RESEND_API_KEY = clé API Resend (re_...) -// - env.RESEND_FROM_EMAIL = adresse expéditrice (domaine vérifié chez Resend) -// - env.RESEND_FROM_NAME = nom affiché à l'expéditeur (ex: "MVA Global Fret") -// - env.SITE_URL = base URL du site (ex: "https://mva-globalfret.com") -// -// API doc : https://resend.com/docs/api-reference/emails/send-email - -const RESEND_API = 'https://api.resend.com/emails'; - -async function resendSend(env, { to, subject, html }) { - if (!env.RESEND_API_KEY) { - throw new Error('RESEND_API_KEY env var not set'); - } - const fromEmail = env.RESEND_FROM_EMAIL || 'noreply@mva-globalfret.com'; - const fromName = env.RESEND_FROM_NAME || 'MVA Global Fret'; - - const res = await fetch(RESEND_API, { - method: 'POST', - headers: { - 'Content-Type' : 'application/json', - 'Authorization': `Bearer ${env.RESEND_API_KEY}`, - }, - body: JSON.stringify({ - from : `${fromName} <${fromEmail}>`, - to : [to], - subject: subject, - html : html, - }), - }); - if (!res.ok) { - const text = await res.text(); - throw new Error(`Resend ${res.status}: ${text}`); - } - return res.json(); -} - -async function sendVerificationEmail(env, contact, verToken) { - const siteUrl = env.SITE_URL || 'https://mva-globalfret.com'; - const verifyUrl = `${siteUrl}/confirmation.html?token=${verToken}`; - const logoUrl = `${siteUrl}/PNG%20MVA%20GLOBAL%20FRET.png`; - const firstname = escapeHtml(contact.firstname || ''); - - const html = ` - - -
-
- - - - - -
- MVA Global Fret - -
MVA GLOBAL FRET
-
Fret Aérien Paris — Antananarivo
-
-
-
-

Bonjour ${firstname},

-

- Merci pour votre inscription chez MVA Global Fret ! -

-

- Pour finaliser votre inscription et recevoir votre numéro de référence client - ainsi que l'adresse de notre dépôt à Paris, cliquez sur le bouton ci-dessous : -

- -

- Ce lien est valable 24 heures. Si vous n'êtes pas à l'origine de cette inscription, ignorez simplement cet email. -

-

- Si le bouton ne fonctionne pas, copiez ce lien dans votre navigateur :
- ${verifyUrl} -

-
-
- © 2026 MVA Global Fret — Tous droits réservés -
-
- -`; - - return resendSend(env, { - to: contact.email, - subject: 'Confirmez votre inscription chez MVA Global Fret', - html, - }); -} - -async function sendWelcomeViaResend(env, contact) { - const siteUrl = env.SITE_URL || 'https://mva-globalfret.com'; - const logoUrl = `${siteUrl}/PNG%20MVA%20GLOBAL%20FRET.png`; - const firstname = escapeHtml(contact.firstname || ''); - const ref = escapeHtml(contact.reference_client || ''); - const refRaw = contact.reference_client || ''; - - // Format adresse Paris : la 1ère ligne (nom du destinataire) reçoit - // automatiquement la référence client entre parenthèses, comme ça - // le client a directement la bonne forme à recopier sur son colis. - // Support aussi un placeholder {{ref}} si présent dans l'env var. - let parisAddrRaw = env.PARIS_DEPOT_ADDRESS || ''; - if (parisAddrRaw.includes('{{ref}}')) { - parisAddrRaw = parisAddrRaw.replace(/\{\{ref\}\}/g, refRaw); - } else if (refRaw && parisAddrRaw) { - const lines = parisAddrRaw.split('\n'); - lines[0] = `${lines[0]} (${refRaw})`; - parisAddrRaw = lines.join('\n'); - } - const parisAddr = escapeHtml(parisAddrRaw).replace(/\n/g, '
'); - - const html = ` - - -
-
- - - - - -
- MVA Global Fret - -
MVA GLOBAL FRET
-
Fret Aérien Paris — Antananarivo
-
-
-
-

Bonjour ${firstname},

-

- Bienvenu(e) chez MVA Global Fret ! Votre email est confirmé, - votre inscription est désormais active. -

-
-

Votre numéro de référence client :

-

${ref}

-

Conservez ce numéro précieusement.

-
-

L'adresse à Paris pour l'envoi de vos colis :

-
- ${parisAddr} -
-
-

- ⚠️ Important : ne modifiez rien à ces informations. -

-

- Recopiez l'adresse exactement telle qu'elle est indiquée ci-dessus, - sans rien retirer ni ajouter. Votre numéro de référence ${ref} - fait partie intégrante de l'adresse — c'est ce qui garantit que votre colis nous arrive bien. -

-
-

- Pour toute question, contactez-nous :
- 📧 mvaglobalfret@gmail.com
- 📞 +33 7 80 97 08 25 (France) — +261 38 49 737 51 (Madagascar) -

-
-
- © 2026 MVA Global Fret — Tous droits réservés -
-
- -`; - - return resendSend(env, { - to: contact.email, - subject: `Bienvenue chez MVA Global Fret — Votre référence ${ref}`, - html, - }); -} - -// Email "Ravis de vous revoir" pour les clients déjà inscrits qui retentent -// le formulaire de contact. Reprend EXACTEMENT le template original (= avant -// migration EmailJS \xe2\x86\x92 Resend) car son contenu est strat\xc3\xa9gique \xe2\x80\x94 rappel -// adresse Paris + warning anti-modification + r\xe9f\xe9rence client. Seules -// modifications : footer (c) 2025 \xe2\x86\x92 \xc2\xa9 2026, suppression du tag -// "Email sent via EmailJS.com" (obsol\xe8te depuis Resend), URL logo -// pointe vers le nouveau domaine, et adresse Paris injecte la ref via -// le placeholder {{ref}} de PARIS_DEPOT_ADDRESS. -// -// Idempotent c\xf4t\xe9 HubSpot (= z\xe9ro write). -async function sendWelcomeBackViaResend(env, contact) { - const siteUrl = env.SITE_URL || 'https://mva-globalfret.com'; - const logoUrl = `${siteUrl}/PNG%20MVA%20GLOBAL%20FRET.png`; - const firstnameRaw = contact.firstname || ''; - const firstname = escapeHtml(firstnameRaw); - const refRaw = contact.reference_client || ''; - const ref = escapeHtml(refRaw); - - // Construction adresse Paris (= m\xeame logique que sendWelcomeViaResend) : - // injecte la ref client soit via placeholder {{ref}}, soit en l'ajoutant - // entre parenth\xe8ses sur la 1\xe8re ligne (= pattern original "VASTA Mélissa (MVA-XXX)"). - let parisAddrRaw = env.PARIS_DEPOT_ADDRESS || ''; - if (parisAddrRaw.includes('{{ref}}')) { - parisAddrRaw = parisAddrRaw.replace(/\{\{ref\}\}/g, refRaw); - } else if (refRaw && parisAddrRaw) { - const lines = parisAddrRaw.split('\n'); - lines[0] = `${lines[0]} (${refRaw})`; - parisAddrRaw = lines.join('\n'); - } - // 1\xe8re ligne en gras (= match original `VASTA Melissa (MVA-XXX)`) - const addrLines = escapeHtml(parisAddrRaw).split('\n'); - const parisAddrHtml = addrLines.length > 1 - ? `${addrLines[0]}
${addrLines.slice(1).join('
')}` - : escapeHtml(parisAddrRaw); - - const greetingTitle = firstnameRaw - ? `Ravis de vous revoir, ${firstname} !` - : 'Ravis de vous revoir !'; - - const html = `
-
-
-
MVA
MVA GLOBAL FRET
Fret Aerien Paris - Antananarivo
-
-
-

${greetingTitle}

-

Nous avons bien recu votre nouvelle tentative d'inscription. Pas d'inquietude : vous etes deja client chez MVA Global Fret !

-

Voici un rappel de votre numero de reference client :

-
-

VOTRE NUMERO DE REFERENCE CLIENT

-

${ref}

-

Conservez ce numero precieusement.

-
-

L'adresse a Paris pour l'envoi de vos colis est :

-
- ${parisAddrHtml} -
-
-

IMPORTANT : Cette adresse ne doit etre changee sous aucun pretexte. Toute modification empecherait la bonne transmission de votre colis a notre depot a Paris.

-
-

Pour toute question, n'hesitez pas a nous contacter :

- -

A tres bientot pour votre prochain envoi,
L'equipe MVA Global Fret

-
-
- (c) 2026 MVA Global Fret - Antananarivo 101, Madagascar -
-
-
`; - - // Subject : reprend strictement le sujet original "Ravis de vous revoir, [firstname] !" - // (= en cas de firstname vide, fallback sans virgule). - const subjectFirstname = firstnameRaw.replace(/[\r\n]/g, '').trim(); - return resendSend(env, { - to: contact.email, - subject: subjectFirstname - ? `Ravis de vous revoir, ${subjectFirstname} !` - : 'Ravis de vous revoir !', - html, - }); -} - -function escapeHtml(s) { - return String(s) - .replace(/&/g, '&').replace(//g, '>') - .replace(/"/g, '"').replace(/'/g, '''); -} - -// ============================================================= -// 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 -// ============================================================= - -function jsonResponse(data, status = 200) { - return new Response(JSON.stringify(data), { - status, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - }); -} diff --git a/cloudflare-worker/wrangler.toml b/cloudflare-worker/wrangler.toml deleted file mode 100644 index c385fb7..0000000 --- a/cloudflare-worker/wrangler.toml +++ /dev/null @@ -1,19 +0,0 @@ -name = "mva-hubspot-proxy" -main = "hubspot-proxy.js" -compatibility_date = "2026-04-01" -workers_dev = true - -# KV namespace — placeholder ID, populated at deploy time (Phase D3) -[[kv_namespaces]] -binding = "WELCOME_KV" -id = "c02656ba22064923ab1c6db06b0f4a56" - -# Required secrets to configure via `wrangler secret put` : -# - HUBSPOT_TOKEN -# - RESEND_API_KEY -# - RESEND_FROM_EMAIL -# - RESEND_FROM_NAME -# - PARIS_DEPOT_ADDRESS -# - TURNSTILE_SECRET -# - SITE_URL -# (BREVO_* and EMAILJS_* deprecated — removed in this version)