Files
EM2_ERP/em2rp/functions/sendAlertEmail.js
T

266 lines
7.4 KiB
JavaScript

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 `
<html>
<body>
<h2>${data.alertTitle}</h2>
<p>${data.alertMessage}</p>
<a href="${data.actionUrl}">Voir l'alerte</a>
</body>
</html>
`;
}
}