const {onCall} = require("firebase-functions/v2/https"); const admin = require("firebase-admin"); const nodemailer = require("nodemailer"); const handlebars = require("handlebars"); const fs = require("fs").promises; const path = require("path"); const {getSmtpConfig, EMAIL_CONFIG} = require("./utils/emailConfig"); /** * Envoie un email d'alerte à un utilisateur * Appelé par le client Dart via callable function */ exports.sendAlertEmail = onCall({ region: "europe-west9", cors: true, }, async (request) => { // Vérifier l'authentification if (!request.auth) { throw new Error("L'utilisateur doit être authentifié"); } const {alertId, userId, templateType} = request.data; if (!alertId || !userId) { throw new Error("alertId et userId sont requis"); } try { // Récupérer l'alerte depuis Firestore const alertDoc = await admin.firestore() .collection("alerts") .doc(alertId) .get(); if (!alertDoc.exists) { throw new Error("Alerte introuvable"); } const alert = alertDoc.data(); // Récupérer l'utilisateur const userDoc = await admin.firestore() .collection("users") .doc(userId) .get(); if (!userDoc.exists) { throw new Error("Utilisateur introuvable"); } const user = userDoc.data(); // Vérifier les préférences email de l'utilisateur const prefs = user.notificationPreferences || {}; if (!prefs.emailEnabled) { console.log(`Email désactivé pour l'utilisateur ${userId}`); return {success: true, skipped: true, reason: "email_disabled"}; } // Vérifier la préférence pour ce type d'alerte const alertType = alert.type; const shouldSend = checkAlertPreference(alertType, prefs); if (!shouldSend) { console.log(`Type d'alerte ${alertType} désactivé pour ${userId}`); return {success: true, skipped: true, reason: "alert_type_disabled"}; } // Préparer les données pour le template const templateData = await prepareTemplateData(alert, user); // Rendre le template HTML const html = await renderTemplate( templateType || "alert-individual", templateData, ); // Configurer le transporteur SMTP const transporter = nodemailer.createTransporter(getSmtpConfig()); // Envoyer l'email const info = await transporter.sendMail({ from: `"${EMAIL_CONFIG.from.name}" <${EMAIL_CONFIG.from.address}>`, to: user.email, replyTo: EMAIL_CONFIG.replyTo, subject: getEmailSubject(alert), html: html, // Fallback texte brut text: alert.message, }); console.log("Email envoyé:", info.messageId); // Marquer l'email comme envoyé dans l'alerte await alertDoc.ref.update({ emailSent: true, emailSentAt: admin.firestore.FieldValue.serverTimestamp(), }); return { success: true, messageId: info.messageId, skipped: false, }; } catch (error) { console.error("Erreur envoi email:", error); throw new Error(`Erreur lors de l'envoi de l'email: ${error.message}`); } }); /** * Vérifie si l'utilisateur souhaite recevoir ce type d'alerte */ function checkAlertPreference(alertType, preferences) { const typeMapping = { "EVENT_CREATED": "eventsNotifications", "EVENT_MODIFIED": "eventsNotifications", "EVENT_CANCELLED": "eventsNotifications", "LOST": "equipmentNotifications", "EQUIPMENT_MISSING": "equipmentNotifications", "MAINTENANCE_REMINDER": "maintenanceNotifications", "STOCK_LOW": "stockNotifications", }; const prefKey = typeMapping[alertType]; return prefKey ? (preferences[prefKey] !== false) : true; } /** * Prépare les données pour le template */ async function prepareTemplateData(alert, user) { const data = { userName: `${user.firstName || ""} ${user.lastName || ""}`.trim() || "Utilisateur", alertTitle: getAlertTitle(alert.type), alertMessage: alert.message, isCritical: alert.severity === "CRITICAL", actionUrl: `${EMAIL_CONFIG.appUrl}${alert.actionUrl || "/alerts"}`, appUrl: EMAIL_CONFIG.appUrl, unsubscribeUrl: `${EMAIL_CONFIG.appUrl}/my_account?tab=notifications`, year: new Date().getFullYear(), subject: getEmailSubject(alert), }; // Ajouter des détails selon le type d'alerte if (alert.eventId) { try { const eventDoc = await admin.firestore() .collection("events") .doc(alert.eventId) .get(); if (eventDoc.exists) { const event = eventDoc.data(); data.eventName = event.Name; if (event.StartDateTime) { const date = event.StartDateTime.toDate(); data.eventDate = date.toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric", }); } } } catch (error) { console.error("Erreur récupération événement:", error); } } if (alert.equipmentId) { try { const eqDoc = await admin.firestore() .collection("equipments") .doc(alert.equipmentId) .get(); if (eqDoc.exists) { data.equipmentName = eqDoc.data().name; } } catch (error) { console.error("Erreur récupération équipement:", error); } } return data; } /** * Génère le titre de l'email selon le type d'alerte */ function getEmailSubject(alert) { const subjects = { "EVENT_CREATED": "📅 Nouvel événement créé", "EVENT_MODIFIED": "📝 Événement modifié", "EVENT_CANCELLED": "❌ Événement annulé", "LOST": "🔴 Alerte critique : Équipement perdu", "EQUIPMENT_MISSING": "⚠️ Équipement manquant", "MAINTENANCE_REMINDER": "🔧 Rappel de maintenance", "STOCK_LOW": "📦 Stock faible", }; return subjects[alert.type] || "🔔 Nouvelle alerte - EM2 Events"; } /** * Génère le titre pour le corps de l'email */ function getAlertTitle(type) { const titles = { "EVENT_CREATED": "Nouvel événement créé", "EVENT_MODIFIED": "Événement modifié", "EVENT_CANCELLED": "Événement annulé", "LOST": "Équipement perdu", "EQUIPMENT_MISSING": "Équipement manquant", "MAINTENANCE_REMINDER": "Maintenance requise", "STOCK_LOW": "Stock faible", }; return titles[type] || "Nouvelle alerte"; } /** * Rend un template HTML avec Handlebars */ async function renderTemplate(templateName, data) { try { // Lire le template de base const basePath = path.join(__dirname, "templates", "base-template.html"); const baseTemplate = await fs.readFile(basePath, "utf8"); // Lire le template de contenu const contentPath = path.join( __dirname, "templates", `${templateName}.html`, ); const contentTemplate = await fs.readFile(contentPath, "utf8"); // Compiler les templates const compileContent = handlebars.compile(contentTemplate); const compileBase = handlebars.compile(baseTemplate); // Rendre le contenu const renderedContent = compileContent(data); // Rendre le template de base avec le contenu return compileBase({ ...data, content: renderedContent, }); } catch (error) { console.error("Erreur rendu template:", error); // Fallback vers un template simple return `
${data.alertMessage}
Voir l'alerte `; } }