feat(api): migrate Worker mva-hubspot-proxy → mva-api /leads/* routes #12
@ -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.');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user