2026-04-23 19:19:31 +02:00

9.0 KiB

Flux "Contact" — notification par email (Brevo)

Créé : 2026-04-23
Statut : en production

Vue d'ensemble

Le formulaire de contact du site envoie un email de notification sur la boîte Gmail personnelle (grascalvet.fernand@gmail.com), via l'API HTTP transactionnelle de Brevo.

Plus aucun passage par Strapi pour les messages. Le content-type message a été supprimé (ainsi que la page admin associée) le 2026-04-23. La table SQLite messages reste présente en base pour l'historique, mais n'est plus écrite ni lue.

Visiteur → /contact (Next.js front)
           ↓
         POST /api/contact  (Next.js App Router, server runtime)
           ↓
         Validation + honeypot + rate-limit
           ↓
         POST https://api.brevo.com/v3/smtp/email
           ↓
         Gmail : CONTACT_TO_EMAIL

Pourquoi cette architecture

Simplification par rapport à l'ancien flux

Avant : Front → Strapi POST /api/messages → SQLite + consultation via /admin/messages (page publique, non protégée).

Problèmes :

  • Pas de notification : il fallait penser à consulter l'admin pour voir les nouveaux messages.
  • Page admin exposée : /admin/messages accessible à n'importe qui en clair (pas d'authentification). Fuite potentielle d'emails privés des visiteurs.
  • Content-type dédié pour un usage limité (3 champs, pas d'historique utile).

Après : notification directe sur Gmail, zéro stockage intermédiaire, zéro page à sécuriser.

Pourquoi l'API HTTP Brevo plutôt que SMTP

Discussion menée le 2026-04-23. Résumé :

Critère SMTP API HTTP
Simplicité plugin Strapi (custom)
Port potentiellement bloqué oui non (HTTPS 443)
Débogage JSON non
Notre besoin (1 endroit, 1 mail) ok ok

Comme on ne passe plus par Strapi, l'avantage "plugin Strapi" disparaît. On a pris l'API HTTP pour rester 100 % en HTTPS (aucun souci de port) et avoir des erreurs JSON lisibles.

Fichiers concernés

Fichier Rôle
app/api/contact/route.ts Endpoint serveur Next.js qui reçoit le form et appelle Brevo
app/components/ContactForm.tsx UI côté client, honeypot, gestion du status
.env.local Secrets + config (non committé)
.env.example Template documentaire des variables à remplir

Variables d'environnement

À définir dans .env.local (voir .env.example pour la structure) :

Variable Description Où l'obtenir
BREVO_API_KEY Clé API Brevo app.brevo.com → SMTP & API → API Keys
CONTACT_FROM_EMAIL Expéditeur vérifié Senders, Domains & IPs → Senders (pastille SPF/DKIM verte)
CONTACT_FROM_NAME Nom affiché dans Gmail Texte libre, ex. "Portfolio — nouveau message"
CONTACT_TO_EMAIL Destinataire final L'adresse Gmail personnelle
CONTACT_TO_NAME Nom du destinataire Texte libre

L'expéditeur doit être vérifié dans Brevo : sinon Brevo renvoie une erreur au POST. On peut réutiliser l'expéditeur newsletter (pas de cloisonnement côté Brevo) et surcharger le nom d'affichage via CONTACT_FROM_NAME.

Anti-abus

Honeypot

Un champ caché website est ajouté au formulaire :

  • Invisible aux humains (position: absolute; left: -10000px; width: 1px).
  • Masqué aux lecteurs d'écran (aria-hidden="true").
  • Ignoré par le clavier (tabindex="-1", autocomplete="off").

Les bots qui parsent le DOM remplissent systématiquement tous les champs texte. Côté serveur, si website est non vide :

  • On renvoie un 200 silencieux (pas de signal d'erreur aux bots → ils ne retentent pas).
  • On n'envoie pas d'email.
  • On logue un warning avec l'IP pour monitoring.

Rate-limit

app/api/contact/route.ts limite à 3 envois par IP par fenêtre de 10 minutes, via une Map en mémoire :

const RATE_LIMIT_MAX = 3;
const RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000;

Au-delà : réponse 429 Too Many Requests avec header Retry-After.

Limite connue : en déploiement serverless avec plusieurs instances (Vercel free, Cloudflare Workers), la Map n'est pas partagée — le rate-limit est "best effort". Pour un portfolio, c'est acceptable. Pour du vrai ship : passer à Redis ou Upstash (wrapper compatible serverless).

Validation

Côté serveur (défense en profondeur, jamais faire confiance au client) :

  • Champs requis non vides : name, email, message.
  • Longueurs max : name ≤ 120, email ≤ 160, message ≤ 5000 caractères (retourne 413).
  • Format email : regex ^[^\s@]+@[^\s@]+\.[^\s@]+$ (pragmatique, pas RFC 5322 stricte).

Les codes d'erreur renvoyés au client sont en UPPER_SNAKE_CASE (stables pour l'i18n future) : MISSING_FIELDS, INVALID_EMAIL, TOO_LONG, RATE_LIMITED, BREVO_ERROR, BREVO_UNREACHABLE, SERVER_MISCONFIGURED, INVALID_JSON.

Format de l'email reçu

  • Sujet : Nouveau message portfolio — {Nom du visiteur}
  • Expéditeur : {CONTACT_FROM_NAME} <{CONTACT_FROM_EMAIL}>
  • Reply-To : email du visiteur → cliquer "Répondre" dans Gmail écrit directement au visiteur.
  • Corps HTML : carte Stitch simplifiée (palette primary / secondary) avec table récap (nom, email, date, IP) + zone message pre-wrap.
  • Corps texte (fallback) : version plain text équivalente.
  • Tags Brevo : ["portfolio", "contact-form"] pour filtrer dans les statistiques Brevo si besoin.

Procédure de test

Local (développement)

  1. S'assurer que .env.local contient les 5 variables requises + une clé Brevo valide.
  2. npm run dev dans le repo Next (port 3000).
  3. /contact → remplir le formulaire avec une adresse email qu'on contrôle (pour vérifier le Reply-To).
  4. Vérifier :
    • Feedback visuel "Message envoyé" en succès Stitch vert.
    • Réception dans Gmail avec expéditeur = CONTACT_FROM_NAME.
    • Clic "Répondre" → destinataire = l'email saisi dans le formulaire.

Honeypot

  1. Ouvrir la console DevTools, onglet Elements.
  2. Trouver l'input caché <input name="website"> (dans un div position: absolute; left: -10000px).
  3. Y saisir une valeur (ex. "bot").
  4. Soumettre le formulaire normalement.
  5. Résultat attendu : UI affiche "Message envoyé" (succès silencieux) MAIS aucun email n'arrive dans Gmail.
  6. Vérifier le warning serveur dans les logs Next ([/api/contact] Honeypot déclenché).

Rate-limit

  1. Envoyer un formulaire valide 4 fois de suite (modifier juste le message à chaque fois).
  2. Au 4ᵉ envoi : réponse 429, UI affiche "Trop d'envois depuis votre IP. Réessayez dans quelques minutes."
  3. Attendre 10 min puis réessayer → doit repasser.

Nettoyage de l'ancienne table SQLite (optionnel)

La table messages existe toujours dans cmsbackend/.tmp/data.db (ou le fichier configuré). Strapi ne la touche plus. Pour la supprimer proprement :

# Arrêter Strapi d'abord !
cd cmsbackend
sqlite3 .tmp/data.db "DROP TABLE IF EXISTS messages;"
sqlite3 .tmp/data.db "DROP TABLE IF EXISTS messages_cmps;"
# Reprise normale
npm run develop

Non fait par défaut : la table reste orpheline mais inoffensive. Ça évite toute régression en cas de rollback éventuel.

Quotas Brevo

Le plan gratuit Brevo inclut traditionnellement 300 emails transactionnels par jour, partagés avec la newsletter. Pour un portfolio qui reçoit 0 à 5 messages par semaine, c'est largement suffisant.

Si le site reçoit un jour beaucoup plus de trafic (ou si Brevo change ses quotas gratuits), surveiller :

  • Brevo → Transactional → Statistics : nombre d'envois, taux d'erreur.
  • Brevo → SMTP & API → Statistics : quota quotidien consommé.

Rollback

Si le flux Brevo tombe en panne (compte suspendu, quota dépassé, clé compromise) et qu'on veut temporairement retomber sur le flux Strapi ancien :

  1. git checkout <commit-pre-refonte-contact> sur les fichiers : app/api/contact/route.ts (supprimer), app/components/ContactForm.tsx (restaurer sendMessage), app/utils/sendMessage.ts (restaurer), cmsbackend/src/api/message/ (restaurer les 4 fichiers), et app/admin/messages/page.tsx si on veut la page de consultation.
  2. Re-démarrer Strapi pour qu'il régénère le content-type en mémoire.
  3. Vérifier les permissions publiques sur POST /api/messages dans l'admin Strapi (peut avoir besoin d'être recoché).

Préférable : renouveler la clé Brevo plutôt que rollback.

Sécurité — rappels

  • Ne jamais committer .env.local. Déjà dans .gitignore (.env*).
  • Si la clé Brevo fuit (exposée dans un commit, un chat, un screenshot) : la supprimer immédiatement sur Brevo et en générer une nouvelle. Une clé compromise permet d'envoyer des emails frauduleux depuis l'expéditeur vérifié, ce qui peut faire blacklister le domaine.
  • L'IP du visiteur est loguée dans l'email (pour abus). À supprimer des emails si on veut un template plus "commercial".