All files / src/routes contact.js

0% Statements 0/35
0% Branches 0/36
0% Functions 0/2
0% Lines 0/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111                                                                                                                                                                                                                             
/**
 * Route Contact — Formulaire de contact public.
 *
 * POST /contact — Envoie un email a l'equipe IliaCloud.
 *
 * Pas d'authentification requise (formulaire public).
 * Rate limite pour eviter le spam.
 * L'adresse de destination est en variable d'environnement (pas dans le code).
 *
 * @module routes/contact
 */
 
const express = require('express');
const rateLimit = require('express-rate-limit');
const { sendEmail } = require('../services/email');
 
const router = express.Router();
 
// Rate limit : 3 messages par IP par heure
const contactLimiter = rateLimit({
  windowMs: 60 * 60 * 1000,
  max: 3,
  message: { error: 'Trop de messages envoyes. Reessayez dans une heure.' },
});
 
// Validation email — quantificateurs bornes pour eviter tout backtracking excessif (ReDoS-safe)
const EMAIL_REGEX = /^[^\s@]{1,64}@[^\s@]{1,253}\.[^\s@]{1,63}$/;
 
// ─── POST /contact ──────────────────────────────────────────────────────────
 
router.post('/', contactLimiter, async (req, res, next) => {
  try {
    const { name, email, subject, message } = req.body;
 
    // Validation
    if (!name || typeof name !== 'string' || name.trim().length === 0) {
      return res.status(400).json({ error: 'Nom requis.' });
    }
    if (name.length > 100) {
      return res.status(400).json({ error: 'Nom trop long (max 100 caracteres).' });
    }
    if (!email || !EMAIL_REGEX.test(email)) {
      return res.status(400).json({ error: 'Email invalide.' });
    }
    if (email.length > 254) {
      return res.status(400).json({ error: 'Email trop long.' });
    }
    if (!message || typeof message !== 'string' || message.trim().length === 0) {
      return res.status(400).json({ error: 'Message requis.' });
    }
    if (message.length > 5000) {
      return res.status(400).json({ error: 'Message trop long (max 5000 caracteres).' });
    }
    if (subject && subject.length > 200) {
      return res.status(400).json({ error: 'Sujet trop long (max 200 caracteres).' });
    }
 
    // Anti-spam basique : honeypot (champ invisible dans le formulaire)
    if (req.body.website) {
      // Bot qui remplit le champ invisible — on simule un succes sans envoyer
      return res.json({ message: 'Message envoye.' });
    }
 
    const contactEmail = process.env.CONTACT_EMAIL || process.env.SMTP_USER;
    const subjectLine = subject
      ? `[Contact IliaCloud] ${subject.trim()}`
      : `[Contact IliaCloud] Message de ${name.trim()}`;
 
    const html = `
      <div style="font-family: -apple-system, sans-serif; max-width: 600px;">
        <h2 style="color: #6366f1;">Nouveau message de contact</h2>
        <table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
          <tr><td style="padding: 8px 0; color: #64748b; width: 80px;">Nom</td><td style="padding: 8px 0; font-weight: 600;">${escapeHtml(name.trim())}</td></tr>
          <tr><td style="padding: 8px 0; color: #64748b;">Email</td><td style="padding: 8px 0;"><a href="mailto:${escapeHtml(email)}">${escapeHtml(email)}</a></td></tr>
          ${subject ? `<tr><td style="padding: 8px 0; color: #64748b;">Sujet</td><td style="padding: 8px 0;">${escapeHtml(subject.trim())}</td></tr>` : ''}
        </table>
        <div style="background: #f8fafc; border-radius: 8px; padding: 16px; margin-top: 16px; white-space: pre-wrap; line-height: 1.6;">
          ${escapeHtml(message.trim())}
        </div>
        <p style="color: #94a3b8; font-size: 12px; margin-top: 24px;">
          Envoye depuis le formulaire de contact IliaCloud — IP: ${req.ip}
        </p>
      </div>
    `;
 
    const sent = await sendEmail({
      to: contactEmail,
      subject: subjectLine,
      html,
      text: `Nom: ${name.trim()}\nEmail: ${email}\nSujet: ${subject || '-'}\n\n${message.trim()}`,
    });
 
    if (!sent) {
      return res.status(503).json({ error: 'Service email temporairement indisponible.' });
    }
 
    res.json({ message: 'Message envoye. Nous vous repondrons rapidement.' });
  } catch (err) {
    next(err);
  }
});
 
/**
 * Echappe le HTML pour eviter les injections XSS dans le template email.
 */
function escapeHtml(str) {
  return str.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
}
 
module.exports = router;