From 483195711eff9234b2228fb3556c385abad0e933 Mon Sep 17 00:00:00 2001 From: serge Date: Thu, 7 May 2026 15:27:41 +0300 Subject: [PATCH] refactor(worker): Brevo+EmailJS ? Resend + remove cron + fix github.io URLs (#3) --- cloudflare-worker/.gitignore | 1 + cloudflare-worker/DEPLOIEMENT.md | 176 +++++++---------- cloudflare-worker/hubspot-proxy.js | 295 ++++++----------------------- cloudflare-worker/wrangler.toml | 19 ++ 4 files changed, 147 insertions(+), 344 deletions(-) create mode 100644 cloudflare-worker/.gitignore create mode 100644 cloudflare-worker/wrangler.toml diff --git a/cloudflare-worker/.gitignore b/cloudflare-worker/.gitignore new file mode 100644 index 0000000..b75a0fa --- /dev/null +++ b/cloudflare-worker/.gitignore @@ -0,0 +1 @@ +.wrangler/ diff --git a/cloudflare-worker/DEPLOIEMENT.md b/cloudflare-worker/DEPLOIEMENT.md index 7890168..cb60e7b 100644 --- a/cloudflare-worker/DEPLOIEMENT.md +++ b/cloudflare-worker/DEPLOIEMENT.md @@ -1,38 +1,50 @@ -# Déploiement Cloudflare Worker — Welcome cron MVA +# Déploiement Cloudflare Worker — Double opt-in MVA via Resend -Ce Worker fait deux choses : -1. **Proxy HubSpot** (déjà fonctionnel — vérification doublon + numéro de référence) -2. **Cron Welcome** (NOUVEAU — envoie l'email de bienvenue **après** que le client a cliqué sur "Confirmer" dans le mail HubSpot de double opt-in) +Ce Worker gère le flow double opt-in du formulaire de contact : -L'email de bienvenue contient le **numéro de référence client** ET (à ajouter dans le template EmailJS) **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. +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 (10 min) +## É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` (de ce repo) +4. Tout sélectionner et remplacer par le contenu de `cloudflare-worker/hubspot-proxy.js` 5. Cliquer **Déployer / Save and deploy** -### 2. Variables d'environnement +### 2. Secrets -Dans **Paramètres → Variables et secrets**, ajouter (si pas déjà là) : +Dans **Paramètres → Variables et secrets** (ou via CLI : `wrangler secret put `) : | Nom | Valeur | |-----|--------| -| `HUBSPOT_TOKEN` | `pat-eu1-...` (existant — vérifier que le scope est read+write contacts) | -| `EMAILJS_PUBLIC_KEY` | `8KUlaQ7BDVIbkZRyP` | -| `EMAILJS_SERVICE_ID` | `service_aeamo3x` | -| `EMAILJS_TEMPLATE_ID` | `template_s1kr2et` | +| `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 que tu veux protéger. Elle ne quitte jamais Cloudflare/EmailJS et n'arrive au client que dans le mail de bienvenue, qui n'est envoyé qu'aux contacts qui ont confirmé leur email. - -> Si une variable n'est pas définie, le Worker utilise le fallback hardcodé. +> `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) @@ -43,100 +55,54 @@ Dans **Paramètres → Stockage et bases de données → Bindings KV** : 3. Namespace : **Créer** un nouveau namespace nommé `mva-welcome-tracker` 4. Sauvegarder -### 4. Cron Trigger — toutes les 5 minutes +Si déployé via `wrangler` : mettre à jour `wrangler.toml` avec l'ID du namespace KV créé (remplacer `REPLACE_AT_DEPLOY_TIME`). -Dans **Paramètres → Déclencheurs (Triggers) → Cron Triggers** : - -1. Cliquer **Add Cron Trigger** -2. Schedule : `*/5 * * * *` -3. Sauvegarder - -### 5a. EmailJS — mettre à jour le template pour inclure l'adresse Paris - -1. Aller sur https://dashboard.emailjs.com/admin/templates -2. Cliquer sur le template `template_s1kr2et` -3. Modifier le contenu pour inclure les variables suivantes : - - `{{firstname}}` — prénom du client - - `{{reference_client}}` — sa référence (ex : `MVA-001`) - - `{{paris_address}}` — l'adresse complète du dépôt à Paris -4. Exemple de corps d'email recommandé : - -``` -Bonjour {{firstname}}, - -Bienvenue chez MVA Global Fret ! Votre inscription est confirmée. - -══════════════════════════════════════════════ -VOTRE NUMÉRO DE RÉFÉRENCE CLIENT -{{reference_client}} -══════════════════════════════════════════════ - -Conservez précieusement ce numéro — il vous permet de suivre vos colis -et nous facilite la prise en charge. - -📦 ADRESSE DU DÉPÔT À PARIS - -{{paris_address}} - -⚠️ Important : indiquez bien votre numéro de référence -({{reference_client}}) sur chaque colis que vous nous envoyez. -Cela nous permet de vous identifier rapidement. - -Pour toute question, contactez-nous via Messenger ou par WhatsApp. - -— L'équipe MVA Global Fret -+261 38 49 737 51 -``` - -5. Sauvegarder le template - -### 5b. EmailJS — autoriser les appels serveur (CRITIQUE) - -Sans cette étape, le Worker ne pourra pas envoyer les emails (erreur 403). - -1. Aller sur https://dashboard.emailjs.com/admin/account -2. Onglet **Security** -3. Décocher **"Allow EmailJS API for non-browser applications"** - - Le terme est trompeur : décocher autorise les appels serveur - - (Cocher = bloquer les appels serveur pour anti-spam) -4. Sauvegarder - -### 6. Vérifier le scope HubSpot +### 4. Vérifier le scope HubSpot Le token HubSpot doit avoir : -- ✅ `crm.objects.contacts.read` -- ✅ `crm.objects.contacts.write` (NOUVEAU — pour mettre à jour la propriété en cas de besoin) -- ✅ `crm.lists.read` (déjà bon) + +- `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 forcer une exécution du cron sans attendre 5 min : +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.mvaglobalfret.workers.dev \ +curl -X POST https://mva-hubspot-proxy..workers.dev \ -H "Content-Type: application/json" \ - -d '{"action":"triggerWelcomeQueue"}' + -d '{ + "action": "requestVerification", + "firstname": "Test", + "lastname": "User", + "email": "test@example.com", + "phone": "+33000000000", + "address": "Test address", + "turnstile_token": "" + }' ``` Réponse attendue : ```json -{ "ok": true, "stats": { "scanned": 3, "sent": 1, "skipped": 2, "errors": 0 } } +{ "ok": true } ``` -- `scanned` : combien de contacts confirmés ont été inspectés -- `sent` : combien d'emails de bienvenue ont été envoyés cette fois-ci -- `skipped` : déjà envoyés précédemment (KV tracking) -- `errors` : envois qui ont échoué +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 cron avec les stats. +Cloudflare → ton Worker → **Logs (en temps réel)** : tu verras chaque exécution du Worker. --- @@ -144,42 +110,40 @@ Cloudflare → ton Worker → **Logs (en temps réel)** : tu verras chaque exéc | Symptôme | Cause probable | Fix | |---|---|---| -| `EmailJS 403: API access disabled` | Étape 5 pas faite | Décocher "Allow API for non-browser" sur EmailJS | -| `HubSpot 403` | Token n'a pas le scope write | Regénérer token avec scope contacts.write | +| `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 | -| Le cron ne tourne pas | Cron trigger pas activé | Vérifier *Triggers → Cron Triggers* | -| KV: `env.WELCOME_KV is undefined` | Binding pas créé | Vérifier *Storage & Databases → KV bindings* | -| Aucun email envoyé même après confirmation | Le filtre HubSpot ne matche pas | Inspecter dans Logs : voir si `searchConfirmedContacts` retourne des résultats | +| `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 finale +## Architecture ``` ┌──────────────────────────────────────────────────────────────────┐ │ 1. User remplit le formulaire sur contact.html │ │ ↓ │ -│ 2. form-handler.js soumet à HubSpot (Forms API) │ +│ 2. form-handler.js → POST action: requestVerification │ │ ↓ │ -│ 3. HubSpot crée le contact AVEC reference_client │ -│ ↓ │ -│ 4. HubSpot envoie l'email de double opt-in (sujet + bouton │ -│ « Confirmer » seulement — PAS la référence ni l'adresse) │ +│ 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 » ─────── │ │ ↓ │ -│ 5. HubSpot met à jour hs_emailconfirmationstatus = CONFIRMED │ +│ 4. confirmation.html → POST action: verifyToken │ │ ↓ │ -│ 6. Cron Cloudflare (toutes les 5 min) : │ -│ - cherche les contacts CONFIRMED non encore welcomed │ -│ - vérifie KV : welcomed:{email} │ -│ - envoie email via EmailJS REST API │ -│ (template contient : prénom, ref, adresse Paris) │ -│ - écrit welcomed:{email} dans KV │ +│ 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). │ │ ↓ │ -│ 7. Le client reçoit son welcome email avec sa référence ET │ +│ 6. Le client reçoit son welcome email avec sa référence ET │ │ l'adresse de dépôt à Paris. │ └──────────────────────────────────────────────────────────────────┘ ``` -**Conclusion** : il est désormais impossible pour un bot de récupérer ta référence client ou ton adresse Paris sans avoir au préalable un email valide ET cliqué sur le lien de confirmation. Anti-spam blindé. ✅ +**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 index a159920..c3a9bcb 100644 --- a/cloudflare-worker/hubspot-proxy.js +++ b/cloudflare-worker/hubspot-proxy.js @@ -1,69 +1,42 @@ // ============================================================ -// MVA Global Fret — Cloudflare Worker : Proxy HubSpot + Welcome cron +// MVA Global Fret — Cloudflare Worker : Proxy HubSpot + double opt-in via Resend // ============================================================ -// Ce Worker fait deux choses : +// Ce Worker gère le formulaire de contact via un flow double opt-in : // -// 1) Proxy HubSpot (via fetch handler, appelé par le navigateur) -// - Vérification doublon par email -// - Génération du prochain numéro de référence séquentiel +// 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) Cron post-confirmation (via scheduled handler, appelé par -// Cloudflare toutes les 5 min) : -// - Cherche les contacts qui ont CONFIRMÉ leur double opt-in -// - Pour chacun, envoie l'email de bienvenue (avec sa référence) -// - Marque le contact dans Cloudflare KV pour ne pas re-envoyer +// 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. // -// L'email de bienvenue n'arrive donc QU'APRÈS que le client a cliqué -// sur "Confirmer" dans le mail HubSpot. La référence client + l'adresse -// du dépôt à Paris ne fuitent jamais avant validation. +// 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 +// DÉPLOIEMENT (Phase D du plan WordPress → static) // ============================================================ // -// Sur https://dash.cloudflare.com/ : +// Voir cloudflare-worker/DEPLOIEMENT.md pour la procédure complète. // -// 1. Workers & Pages → ton Worker mva-hubspot-proxy → Modifier le code -// → coller ce fichier → Déployer +// 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") // -// 2. Workers & Pages → ton Worker → Paramètres → Variables et secrets : -// • HUBSPOT_TOKEN = pat-eu1-... (déjà existant, lecture+écriture contacts) -// • EMAILJS_PUBLIC_KEY = 8KUlaQ7BDVIbkZRyP -// • EMAILJS_SERVICE_ID = service_aeamo3x -// • EMAILJS_TEMPLATE_ID = template_s1kr2et -// -// 3. Workers & Pages → ton Worker → Paramètres → Stockage et bases -// → Bindings KV → Ajouter : -// Variable name : WELCOME_KV -// Namespace : créer "mva-welcome-tracker" -// -// 4. Workers & Pages → ton Worker → Paramètres → Déclencheurs (Triggers) -// → Cron Triggers → Ajouter : -// */5 * * * * (toutes les 5 minutes) -// -// 5. ⚠️ EmailJS : sur https://dashboard.emailjs.com/admin/account → -// Security → décocher "Allow EmailJS API for non-browser applications" -// → la décocher (= autoriser les appels serveur). Sinon le Worker ne -// pourra pas envoyer les emails. -// -// 6. ⚠️ Le token HubSpot doit avoir le scope crm.objects.contacts.write -// en plus du read (pour mettre à jour les propriétés contact). -// → si erreur 403 sur la mise à jour KV/contact, regénérer le token -// avec ce scope sur https://app-eu1.hubspot.com/private-apps/... +// Bindings KV requis : +// • WELCOME_KV → namespace `mva-welcome-tracker` (idempotence verifyToken) // // ============================================================ -// Fallbacks pour les valeurs publiques d'EmailJS (déjà visibles dans le -// JavaScript du site). Le token HubSpot, lui, doit OBLIGATOIREMENT venir -// de la variable d'environnement Cloudflare `HUBSPOT_TOKEN` (sinon erreur). -const FALLBACK_EMAILJS_PUBLIC_KEY = '8KUlaQ7BDVIbkZRyP'; -const FALLBACK_EMAILJS_SERVICE_ID = 'service_aeamo3x'; -const FALLBACK_EMAILJS_TEMPLATE_ID= 'template_s1kr2et'; - const HUBSPOT_API = 'https://api.hubapi.com'; const HUBSPOT_PORTAL_ID = '148163754'; const HUBSPOT_FORM_GUID = '1d9b75c9-8b60-4966-aa18-4bf503452e9a'; -const EMAILJS_API = 'https://api.emailjs.com/api/v1.0/email/send'; const corsHeaders = { 'Access-Control-Allow-Origin' : '*', @@ -98,42 +71,9 @@ export default { return jsonResponse({ nextRef: await getNextRef(token) }); } - // ── action: triggerWelcome (admin/debug, optionnel) ───── - if (action === 'triggerWelcomeQueue') { - const stats = await processWelcomeQueue(env); - return jsonResponse({ ok: true, stats }); - } - - // ── action: sendWelcomeNow ─────────────────────────────── - // Envoi immédiat du welcome email via EmailJS (avec l'adresse - // 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 || '', - email : body.email, - reference_client : body.reference_client || '', - }); - return jsonResponse({ ok: true }); - } catch (err) { - return jsonResponse({ ok: false, error: err.message }, 500); - } - } - // ── action: requestVerification ────────────────────────── // Génère un token unique, stocke TOUTES les données du formulaire en KV, - // et envoie un email de validation via Brevo. Le contact n'est créé + // 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. @@ -231,7 +171,7 @@ export default { { name: 'reference_client', value: refNumber }, ], context: { - pageUri : 'https://mva-global-fret.github.io/site-mva-global-fret/contact.html', + pageUri : 'https://mva-globalfret.com/contact.html', pageName: 'Verified signup (MVA Global Fret)', }, }), @@ -244,7 +184,7 @@ export default { // 3) Envoie le welcome email avec ref + adresse Paris const welcomeContact = { ...tokenData, reference_client: refNumber }; - await sendWelcomeViaBrevo(env, welcomeContact); + await sendWelcomeViaResend(env, welcomeContact); // 4) Marque le token consommé (gardé 7j pour idempotence) await env.WELCOME_KV.put(key, JSON.stringify({ @@ -315,102 +255,12 @@ export default { return jsonResponse({ error: err.message }, 500); } }, - - // ----------------------------------------------------------- - // 2) Handler cron (Cloudflare scheduler, toutes les 5 min) - // ----------------------------------------------------------- - async scheduled(event, env, ctx) { - ctx.waitUntil(processWelcomeQueue(env)); - }, }; -// ============================================================= -// File d'attente : envoi du welcome aux contacts confirmés -// ============================================================= -async function processWelcomeQueue(env) { - const token = env.HUBSPOT_TOKEN; - const stats = { scanned: 0, sent: 0, skipped: 0, errors: 0 }; - - // Liste des contacts qui ont CONFIRMÉ leur opt-in marketing - // Filtre HubSpot : hs_emailconfirmationstatus EQ "CONFIRMED" - const confirmed = await searchConfirmedContacts(token); - - for (const contact of confirmed) { - stats.scanned++; - const props = contact.properties || {}; - const email = (props.email || '').toLowerCase(); - - if (!email) { stats.skipped++; continue; } - - // Idempotence : si on a déjà envoyé, on saute - const kvKey = `welcomed:${email}`; - const already = env.WELCOME_KV ? await env.WELCOME_KV.get(kvKey) : null; - if (already) { stats.skipped++; continue; } - - try { - await sendWelcomeEmail(env, { - firstname : props.firstname || '', - email : email, - reference_client : props.reference_client || '', - }); - - // Marquer comme envoyé dans KV (TTL 1 an pour éviter de garder - // indéfiniment des entrées si quelqu'un se désabonne et se réabonne) - if (env.WELCOME_KV) { - await env.WELCOME_KV.put(kvKey, new Date().toISOString(), { - expirationTtl: 60 * 60 * 24 * 365, - }); - } - stats.sent++; - - } catch (err) { - stats.errors++; - console.warn('[welcome]', email, err.message); - } - } - - return stats; -} - // ============================================================= // HubSpot : recherches & lectures // ============================================================= -// Date avant laquelle les contacts CONFIRMÉS ne déclenchent PAS de welcome. -// Évite de spammer les contacts déjà existants au moment du déploiement -// du nouveau cron. Tout contact créé APRÈS cette date (et qui confirme -// son email) recevra son email de bienvenue normalement. -const WELCOME_CUTOFF_ISO = '2026-05-05T00:00:00Z'; - -async function searchConfirmedContacts(token) { - // On limite à 100 contacts par cron run (largement suffisant pour 1 PME) - 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: 'hs_emailconfirmationstatus', operator: 'EQ', value: 'CONFIRMED' }, - { propertyName: 'reference_client', operator: 'HAS_PROPERTY' }, - { propertyName: 'createdate', operator: 'GTE', value: WELCOME_CUTOFF_ISO }, - ], - }], - properties: ['firstname', 'lastname', 'email', 'reference_client', 'hs_emailconfirmationstatus'], - sorts: [{ propertyName: 'lastmodifieddate', direction: 'DESCENDING' }], - limit: 100, - }), - }); - - if (!res.ok) { - throw new Error(`HubSpot search failed: ${res.status}`); - } - const data = await res.json(); - return data.results || []; -} - async function searchContactByEmail(token, email) { const res = await fetch(`${HUBSPOT_API}/crm/v3/objects/contacts/search`, { method: 'POST', @@ -447,6 +297,9 @@ async function getNextRef(token) { limit: 100, }), }); + if (!res.ok) { + throw new Error(`HubSpot search failed: ${res.status}`); + } const data = await res.json(); let maxNum = 0; (data.results || []).forEach(c => { @@ -460,85 +313,51 @@ async function getNextRef(token) { } // ============================================================= -// EmailJS : envoi serveur via REST API +// Resend : envoi d'emails transactionnels (verification + welcome) // ============================================================= - -async function sendWelcomeEmail(env, params) { - const payload = { - service_id : env.EMAILJS_SERVICE_ID || FALLBACK_EMAILJS_SERVICE_ID, - template_id: env.EMAILJS_TEMPLATE_ID || FALLBACK_EMAILJS_TEMPLATE_ID, - user_id : env.EMAILJS_PUBLIC_KEY || FALLBACK_EMAILJS_PUBLIC_KEY, - template_params: { - firstname : params.firstname, - email : params.email, - reference_client : params.reference_client, - // Adresse du dépôt Paris — définie via l'env var PARIS_DEPOT_ADDRESS - // dans Cloudflare. Si non définie, on envoie un placeholder visible - // pour signaler à l'admin qu'il faut la configurer. - paris_address : env.PARIS_DEPOT_ADDRESS || '[À configurer dans Cloudflare]', - }, - }; - - const res = await fetch(EMAILJS_API, { - method : 'POST', - headers: { 'Content-Type': 'application/json' }, - body : JSON.stringify(payload), - }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(`EmailJS ${res.status}: ${text}`); - } -} - -// ============================================================= -// Brevo (ex-Sendinblue) : envoi d'emails (verification + welcome) -// ============================================================= -// Brevo est utilisé pour l'envoi car il accepte la "single-sender -// verification" : on valide juste une adresse email (mvaglobalfret@gmail.com) -// au lieu de devoir vérifier tout un domaine. Free tier : 300 emails/jour. +// 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 : -// - env.BREVO_API_KEY = clé API Brevo (xkeysib-...) -// - env.BREVO_SENDER_EMAIL = adresse expéditrice validée chez Brevo -// (ex: "mvaglobalfret@gmail.com") -// - env.BREVO_SENDER_NAME = nom affiché à l'expéditeur (ex: "MVA Global Fret") -// - env.SITE_URL = base URL du site (ex: "https://mva-global-fret.github.io/site-mva-global-fret") +// 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://developers.brevo.com/reference/sendtransacemail +// API doc : https://resend.com/docs/api-reference/emails/send-email -const BREVO_API = 'https://api.brevo.com/v3/smtp/email'; +const RESEND_API = 'https://api.resend.com/emails'; -async function brevoSend(env, { to, subject, html }) { - if (!env.BREVO_API_KEY) { - throw new Error('BREVO_API_KEY env var not set'); +async function resendSend(env, { to, subject, html }) { + if (!env.RESEND_API_KEY) { + throw new Error('RESEND_API_KEY env var not set'); } - const senderEmail = env.BREVO_SENDER_EMAIL || 'mvaglobalfret@gmail.com'; - const senderName = env.BREVO_SENDER_NAME || 'MVA Global Fret'; + const fromEmail = env.RESEND_FROM_EMAIL || 'noreply@mva-globalfret.com'; + const fromName = env.RESEND_FROM_NAME || 'MVA Global Fret'; - const res = await fetch(BREVO_API, { + const res = await fetch(RESEND_API, { method: 'POST', headers: { - 'api-key' : env.BREVO_API_KEY, - 'accept' : 'application/json', - 'content-type': 'application/json', + 'Content-Type' : 'application/json', + 'Authorization': `Bearer ${env.RESEND_API_KEY}`, }, body: JSON.stringify({ - sender : { name: senderName, email: senderEmail }, - to : [{ email: to }], - subject : subject, - htmlContent: html, + from : `${fromName} <${fromEmail}>`, + to : [to], + subject: subject, + html : html, }), }); if (!res.ok) { const text = await res.text(); - throw new Error(`Brevo ${res.status}: ${text}`); + throw new Error(`Resend ${res.status}: ${text}`); } return res.json(); } async function sendVerificationEmail(env, contact, verToken) { - const siteUrl = env.SITE_URL || 'https://mva-global-fret.github.io/site-mva-global-fret'; + 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 || ''); @@ -589,15 +408,15 @@ async function sendVerificationEmail(env, contact, verToken) { `; - return brevoSend(env, { + return resendSend(env, { to: contact.email, subject: 'Confirmez votre inscription chez MVA Global Fret', html, }); } -async function sendWelcomeViaBrevo(env, contact) { - const siteUrl = env.SITE_URL || 'https://mva-global-fret.github.io/site-mva-global-fret'; +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 || ''); @@ -672,7 +491,7 @@ async function sendWelcomeViaBrevo(env, contact) { `; - return brevoSend(env, { + return resendSend(env, { to: contact.email, subject: `Bienvenue chez MVA Global Fret — Votre référence ${ref}`, html, diff --git a/cloudflare-worker/wrangler.toml b/cloudflare-worker/wrangler.toml new file mode 100644 index 0000000..c385fb7 --- /dev/null +++ b/cloudflare-worker/wrangler.toml @@ -0,0 +1,19 @@ +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)