const functions = require('firebase-functions'); 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 = functions.https.onCall(async (data, context) => { // Vérifier l'authentification if (!context.auth) { throw new functions.https.HttpsError( 'unauthenticated', 'L\'utilisateur doit être authentifié', ); } const {alertId, userId, templateType} = data; if (!alertId || !userId) { throw new functions.https.HttpsError( 'invalid-argument', '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 functions.https.HttpsError( 'not-found', '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 functions.https.HttpsError( 'not-found', '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 functions.https.HttpsError( 'internal', `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.alertTitle}

${data.alertMessage}

Voir l'alerte `; } }