266 lines
7.4 KiB
JavaScript
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>
|
|
`;
|
|
}
|
|
}
|
|
|