Compare commits

...

2 Commits

Author SHA1 Message Date
Serge RAKOTO HARRY-NAIVO
c27a9ea805 chore(worker): wire up real KV ID + workers_dev=true + harden getNextRef
- wrangler.toml: workers_dev=true (= explicit routing on *.workers.dev)
- wrangler.toml: real KV ID c02656ba2206... after `wrangler kv namespace create WELCOME_KV`
- hubspot-proxy.js getNextRef: guard `!res.ok` before res.json() to surface HubSpot errors
- .gitignore: ignore .wrangler/ local cache dir

Discovered during Phase D2-D3 deploy + smoke testing.

Refs: WordPress \xe2\x86\x92 static migration plan, Phase B PR #3a.
2026-05-07 14:25:25 +02:00
Serge RAKOTO HARRY-NAIVO
e4b4992e67 refactor(worker): migrate Brevo+EmailJS → Resend + remove cron + fix github.io URLs
- Replace Brevo HTTP API calls with Resend (api.resend.com/emails) — aligns with m4s-auth Phase 2.1 SMTP provider
- Remove scheduled handler and processWelcomeQueue (EmailJS cron */5 * * * *)
  → eliminates double-send race window identified in Phase 2 audit
- Rename sendWelcomeViaBrevo → sendWelcomeViaResend
- Fix 3 hardcoded GH Pages URLs (lines ~234, ~541, ~600) → mva-globalfret.com fallback
- Add wrangler.toml: KV binding placeholder, no [triggers] cron, secrets list
- Update DEPLOIEMENT.md: env vars list (Resend in, Brevo+EmailJS out)

DO NOT MERGE until new Cloudflare account + Worker deployment Phase D ready
(merge écarterait l'ancien Worker Melissa actif si on revenait dessus).

Refs: WordPress → static migration plan, Phase B PR #3a.
2026-05-07 00:11:10 +02:00
4 changed files with 147 additions and 344 deletions

1
cloudflare-worker/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.wrangler/

View File

@ -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 <name>`) :
| 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.<account>.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": "<token from Turnstile widget>"
}'
```
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é.

View File

@ -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 <name>`) :
// • 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 <name>`) :
// - 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 }],
from : `${fromName} <${fromEmail}>`,
to : [to],
subject: subject,
htmlContent: html,
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) {
</body>
</html>`;
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) {
</body>
</html>`;
return brevoSend(env, {
return resendSend(env, {
to: contact.email,
subject: `Bienvenue chez MVA Global Fret — Votre référence ${ref}`,
html,

View File

@ -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)