272 lines
7.5 KiB
JavaScript
272 lines
7.5 KiB
JavaScript
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",
|
|
region: "europe-west9",
|
|
}, 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;
|
|
}
|
|
}
|
|
|