Compare commits

..

2 Commits

Author SHA1 Message Date
61397720e8 Merge pull request 'feat(api): migrate Worker mva-hubspot-proxy → mva-api /leads/* routes' (#12) from feat/migrate-worker-to-mva-api into main
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-05-10 14:02:18 +03:00
Serge RAKOTO HARRY-NAIVO
7217f12bd2 feat(api): migrate Worker mva-hubspot-proxy → mva-api /leads/* routes
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).
2026-05-10 11:34:07 +02:00
2 changed files with 51 additions and 47 deletions

View File

@ -1,23 +1,27 @@
// ============================================================ // ============================================================
// MVA Global Fret — Page de confirmation post-validation email // MVA Global Fret — Page de confirmation post-validation email
// ============================================================ // ============================================================
// Cette page est la cible du lien dans l'email de validation // Cette page est la cible du lien dans l'email de validation envoyé
// (envoyé par Brevo après soumission du formulaire). // 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 : // Étapes :
// 1. Lire le token depuis l'URL // 1. Lire le token depuis l'URL
// 2. POST au Worker avec action 'verifyToken' // 2. POST mva-api /leads/verify-token avec { token }
// 3. Worker valide le token, envoie le welcome email (avec ref + // 3. mva-api INSERT le lead en DB, génère la ref MVA-NNN, envoie le
// adresse Paris) via Brevo, puis renvoie OK // welcome email (= ref + adresse Paris) via Resend, puis renvoie
// 4. Page affiche "Inscription confirmée !" // { 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 // Si le token est invalide / expiré / déjà consommé : affichage d'un
// avec invitation à contacter le support. // 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 () => { document.addEventListener('DOMContentLoaded', async () => {
const token = new URLSearchParams(window.location.search).get('token'); const token = new URLSearchParams(window.location.search).get('token');
@ -28,18 +32,20 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
try { try {
const res = await fetch(WORKER_PROXY_URL, { const res = await fetch(`${API_BASE_URL}/leads/verify-token`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'verifyToken', token }), body: JSON.stringify({ token }),
}); });
const data = await res.json(); const data = await res.json();
if (data.ok) { if (data.ok) {
showSuccess(data.reference_client || null); showSuccess(data.reference_client || null);
} else { } else {
// Token expiré, déjà utilisé, ou inconnu // Token invalide, expiré, ou inconnu
showError(data.error === 'Token invalide ou expiré' 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é.' ? 'Ce lien de confirmation a expiré ou a déjà été utilisé.'
: 'Une erreur est survenue lors de la confirmation.'); : 'Une erreur est survenue lors de la confirmation.');
} }

View File

@ -3,23 +3,21 @@
// ============================================ // ============================================
// Frontend logic for contact.html (= inscription form): // Frontend logic for contact.html (= inscription form):
// - validate inputs + Cloudflare Turnstile token // - validate inputs + Cloudflare Turnstile token
// - call Cloudflare Worker mva-hubspot-proxy for HubSpot dedup + // - call mva-api /leads/* routes for dedup check + double opt-in flow
// sending verification email (= action requestVerification) or // (= verification email + welcome / welcome-back emails via Resend)
// "Ravis de vous revoir" email for returning customers (= // - reset Turnstile widget after each API call (= tokens are
// action sendWelcomeBack)
// - reset Turnstile widget after each Worker call (= tokens are
// single-use server-side; without reset, a re-submit silently // single-use server-side; without reset, a re-submit silently
// 403s from Cloudflare's siteverify endpoint) // 403s from Cloudflare's siteverify endpoint)
// //
// All HubSpot/Resend transactions go through the Worker. No direct // Migration 2026-05-10 : remplace l'ancien Cloudflare Worker
// EmailJS / Formspree / HubSpot Forms API calls from the browser. // `mva-hubspot-proxy.sergemind4s.workers.dev` (= décommissionné) par
// les routes mva-api Fastify. La DB Postgres remplace HubSpot Contacts.
// ============================================ // ============================================
// ── PROXY CLOUDFLARE WORKER ────────────────────────────────────── // ── MVA API BASE URL ─────────────────────────────────────────────
// Worker URL (= deployed via wrangler from cloudflare-worker/). // Routes leads servies par mva-api derrière Caddy. CORS strict :
// CORS Access-Control-Allow-Origin: * so the browser can call it // le serveur whitelist explicitement https://mva-globalfret.com.
// directly. const API_BASE_URL = 'https://api.mva.mind4solutions.com';
const WORKER_PROXY_URL = 'https://mva-hubspot-proxy.sergemind4s.workers.dev';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('contactForm'); 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. // Vérifie si l'email existe déjà dans la table leads via mva-api.
// Retourne les propriétés du contact existant, ou null si nouveau // Retourne les propriétés du lead existant, ou null si nouveau
// client / Worker indisponible. // client / API indisponible.
async function checkExistingContact(email) { async function checkExistingContact(email) {
if (!WORKER_PROXY_URL) return null;
try { try {
const res = await fetch(WORKER_PROXY_URL, { const res = await fetch(`${API_BASE_URL}/leads/check-email`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.toLowerCase().trim() }), body: JSON.stringify({ email: email.toLowerCase().trim() }),
}); });
if (!res.ok) return null; if (!res.ok) return null;
const data = await res.json(); 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 { } catch {
return null; return null;
} }
@ -95,18 +97,17 @@ function setupContactForm(form) {
address: form.address.value.trim(), address: form.address.value.trim(),
}; };
// ── ENVOI VERS LE WORKER ───────────────────────────────────── // ── ENVOI VERS MVA-API ────────────────────────────────────────
// Le Worker stocke les données en KV (24h) et envoie un email de // L'API stocke les données en `leads_pending` (24h TTL) et envoie un
// validation via Resend. Le contact n'est créé dans HubSpot QUE // email de validation via Resend. Le lead n'est INSERT en `leads` QUE
// quand l'utilisateur clique sur le lien de confirmation // quand l'utilisateur clique sur le lien de confirmation
// (anti-pollution du CRM). // (anti-pollution DB + anti-bot complémentaire à Turnstile).
let ok = false; let ok = false;
try { try {
const res = await fetch(WORKER_PROXY_URL, { const res = await fetch(`${API_BASE_URL}/leads/request-verification`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
action: 'requestVerification',
...data, ...data,
turnstile_token: window.turnstileToken || '', turnstile_token: window.turnstileToken || '',
}), }),
@ -221,29 +222,26 @@ function showSuccess(_refNumber, _clientData) {
} }
// ── EMAIL "RAVIS DE VOUS REVOIR" (client déjà inscrit) ─────────── // ── EMAIL "RAVIS DE VOUS REVOIR" (client déjà inscrit) ───────────
// Rappelle au client son numéro de référence existant — n'écrit // Rappelle au client son numéro de référence existant — zéro write DB.
// RIEN dans HubSpot. Passe par le Cloudflare Worker (action: // Passe par mva-api /leads/welcome-back qui délègue à Resend.
// sendWelcomeBack) qui délègue à Resend. Anti-bot via Turnstile : // Anti-bot via Turnstile : transmet le token déjà validé au moment du
// transmet le token déjà validé au moment du submit du formulaire. // submit du formulaire.
async function sendWelcomeBackEmail(contact) { async function sendWelcomeBackEmail(contact) {
if (!WORKER_PROXY_URL) return;
if (!contact || !contact.email) return; if (!contact || !contact.email) return;
if (!window.turnstileToken) return; if (!window.turnstileToken) return;
try { try {
await fetch(WORKER_PROXY_URL, { await fetch(`${API_BASE_URL}/leads/welcome-back`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
action : 'sendWelcomeBack',
email : contact.email, email : contact.email,
firstname : contact.firstname || '',
turnstile_token : window.turnstileToken, turnstile_token : window.turnstileToken,
}), }),
}); });
} catch (err) { } catch (err) {
// Erreur réseau : on n'interrompt pas l'UX (le client voit // Erreur réseau : on n'interrompt pas l'UX (le client voit
// déjà sa référence dans le UI). // déjà sa référence dans le UI).
console.warn('Worker sendWelcomeBack failed:', err); console.warn('welcome-back failed:', err);
} }
} }