const {onRequest} = require('firebase-functions/v2/https'); const admin = require('firebase-admin'); const nodemailer = require('nodemailer'); const logger = require('firebase-functions/logger'); const {getSmtpConfig, EMAIL_CONFIG} = require('./utils/emailConfig'); const {renderTemplate, getEmailSubject, getAlertTitle, prepareTemplateData, checkAlertPreference} = require('./utils/emailTemplates'); const auth = require('./utils/auth'); // Configuration CORS const setCorsHeaders = (res, req) => { // Utiliser l'origin de la requête pour permettre les credentials const origin = req.headers.origin || '*'; res.set('Access-Control-Allow-Origin', origin); // N'autoriser les credentials que si on a un origin spécifique (pas '*') if (origin !== '*') { res.set('Access-Control-Allow-Credentials', 'true'); } res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.set('Access-Control-Allow-Headers', 'Authorization, Content-Type, Accept, Origin, X-Requested-With'); res.set('Access-Control-Max-Age', '3600'); }; const withCors = (handler) => { return async (req, res) => { setCorsHeaders(res, req); // Gérer les requêtes preflight OPTIONS immédiatement if (req.method === 'OPTIONS') { res.status(204).send(''); return; } try { await handler(req, res); } catch (error) { logger.error("Unhandled error:", error); if (!res.headersSent) { res.status(500).json({error: error.message}); } } }; }; /** * Crée une alerte et envoie les notifications * Gère tout le processus côté backend de A à Z */ exports.createAlert = onRequest({cors: false, invoker: 'public'}, withCors(async (req, res) => { try { // Vérifier l'authentification const decodedToken = await auth.authenticateUser(req); const data = req.body.data || req.body; const { type, severity, title, message, equipmentId, eventId, actionUrl, metadata, } = data; // Validation des données if (!type || !severity || !message) { res.status(400).json({error: 'type, severity et message sont requis'}); return; } // 1. Déterminer les utilisateurs à notifier const userIds = await determineTargetUsers(type, severity, eventId); if (userIds.length === 0) { res.status(400).json({error: 'Aucun utilisateur à notifier'}); return; } // 2. Créer l'alerte dans Firestore const alertRef = admin.firestore().collection('alerts').doc(); const alertData = { id: alertRef.id, type, severity, title: title || getAlertTitle(type), message, equipmentId: equipmentId || null, eventId: eventId || null, actionUrl: actionUrl || null, metadata: metadata || {}, assignedTo: userIds, createdAt: admin.firestore.FieldValue.serverTimestamp(), createdBy: decodedToken.uid, isRead: false, emailSent: false, status: 'ACTIVE', }; await alertRef.set(alertData); // 3. Envoyer les emails si alerte critique let emailResults = {}; if (severity === 'CRITICAL') { emailResults = await sendAlertEmails(alertRef.id, alertData, userIds); // Mettre à jour le statut d'envoi await alertRef.update({ emailSent: true, emailSentAt: admin.firestore.FieldValue.serverTimestamp(), emailResults, }); } res.status(200).json({ success: true, alertId: alertRef.id, usersNotified: userIds.length, emailsSent: Object.values(emailResults).filter((v) => v).length, }); } catch (error) { logger.error('[createAlert] Erreur:', error); res.status(500).json({error: `Erreur lors de la création de l'alerte: ${error.message}`}); } })); /** * Détermine les utilisateurs à notifier selon le type d'alerte */ async function determineTargetUsers(alertType, severity, eventId) { const db = admin.firestore(); const targetUserIds = new Set(); // 1. Récupérer TOUS les utilisateurs pour déterminer lesquels sont admins const allUsersSnapshot = await db.collection('users').get(); allUsersSnapshot.forEach((doc) => { const user = doc.data(); if (user.role) { // Le rôle peut être une référence Firestore ou une string let rolePath = ''; if (typeof user.role === 'string') { rolePath = user.role; } else if (user.role.path) { rolePath = user.role.path; } else if (user.role._path && user.role._path.segments) { rolePath = user.role._path.segments.join('/'); } // Vérifier si c'est un admin (path = "roles/ADMIN") if (rolePath === 'roles/ADMIN' || rolePath === 'ADMIN') { targetUserIds.add(doc.id); } } }); // 2. Si un événement est lié, ajouter tous les membres de la workforce if (eventId) { try { const eventDoc = await db.collection('events').doc(eventId).get(); if (eventDoc.exists) { const event = eventDoc.data(); const workforce = event.workforce || []; workforce.forEach((member) => { if (member.userId) { targetUserIds.add(member.userId); } }); } else { logger.warn(`[determineTargetUsers] Événement ${eventId} introuvable`); } } catch (error) { logger.error('[determineTargetUsers] Erreur récupération événement:', error); } } return Array.from(targetUserIds); } /** * Envoie les emails d'alerte à tous les utilisateurs */ async function sendAlertEmails(alertId, alertData, userIds) { const results = {}; const transporter = nodemailer.createTransporter(getSmtpConfig()); // Envoyer les emails en parallèle (batch de 5) const batches = []; for (let i = 0; i < userIds.length; i += 5) { batches.push(userIds.slice(i, i + 5)); } for (const batch of batches) { const promises = batch.map(async (userId) => { try { const sent = await sendSingleEmail(transporter, alertId, alertData, userId); results[userId] = sent; } catch (error) { logger.error(`[sendAlertEmails] Erreur email ${userId}:`, error); results[userId] = false; } }); await Promise.all(promises); } return results; } /** * Envoie un email à un utilisateur spécifique */ async function sendSingleEmail(transporter, alertId, alertData, userId) { const db = admin.firestore(); // Récupérer l'utilisateur const userDoc = await db.collection('users').doc(userId).get(); if (!userDoc.exists) { return false; } const user = userDoc.data(); // Vérifier les préférences email const prefs = user.notificationPreferences || {}; if (!prefs.emailEnabled) { return false; } // Vérifier la préférence pour ce type d'alerte if (!checkAlertPreference(alertData.type, prefs)) { return false; } if (!user.email) { return false; } try { // Préparer les données du template const templateData = await prepareTemplateData(alertData, user); // Rendre le template const html = await renderTemplate('alert-individual', templateData); // Envoyer l'email await transporter.sendMail({ from: `"${EMAIL_CONFIG.from.name}" <${EMAIL_CONFIG.from.address}>`, to: user.email, replyTo: EMAIL_CONFIG.replyTo, subject: getEmailSubject(alertData), html: html, text: alertData.message, }); return true; } catch (error) { logger.error(`[sendSingleEmail] Erreur envoi à ${userId}:`, error); return false; } }