/** * EM2RP Cloud Functions * Architecture backend sécurisée et modulaire avec dynamic imports (lazy loading) */ const path = require("path"); require("dotenv").config({path: path.join(__dirname, ".env.local")}); require("dotenv").config({path: path.join(__dirname, ".env")}); const {onRequest, onCall} = require("firebase-functions/v2/https"); const {onSchedule} = require("firebase-functions/v2/scheduler"); const {onDocumentCreated, onDocumentUpdated} = require("firebase-functions/v2/firestore"); const logger = require("firebase-functions/logger"); const admin = require("firebase-admin"); // Initialisation sécurisée de Firebase Admin au démarrage if (!admin.apps.length) { admin.initializeApp(); } const db = admin.firestore(); // Configuration commune pour toutes les fonctions HTTP const httpOptions = { cors: false, invoker: "public", region: "europe-west9", }; // Options dédiées pour les traitements IA potentiellement longs. const aiHttpOptions = { ...httpOptions, timeoutSeconds: 300, memory: "1GiB", }; // Options HTTP spécifiques pour TTS avec CORS activé const ttsHttpOptions = { cors: true, invoker: "public", region: "europe-west9", }; // CORS Middleware const setCorsHeaders = (res, req) => { const origin = req.headers.origin || "*"; res.set("Access-Control-Allow-Origin", origin); 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); 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}); } } }; }; // ============================================================================ // STORAGE // ============================================================================ exports.moveEventFileV2 = onRequest(httpOptions, withCors((req, res) => { return require("./src/storage").moveEventFileV2(req, res); })); // ============================================================================ // EQUIPMENT // ============================================================================ exports.createEquipment = onRequest(httpOptions, withCors((req, res) => { return require("./src/equipments").createEquipment(req, res); })); exports.updateEquipment = onRequest(httpOptions, withCors((req, res) => { return require("./src/equipments").updateEquipment(req, res); })); exports.deleteEquipment = onRequest(httpOptions, withCors((req, res) => { return require("./src/equipments").deleteEquipment(req, res); })); exports.getEquipment = onRequest(httpOptions, withCors((req, res) => { return require("./src/equipments").getEquipment(req, res); })); exports.getEquipmentsByIds = onRequest(httpOptions, withCors((req, res) => { return require("./src/equipments").getEquipmentsByIds(req, res); })); exports.updateEquipmentStatusOnly = onRequest(httpOptions, withCors((req, res) => { return require("./src/equipments").updateEquipmentStatusOnly(req, res); })); exports.updateEquipmentStatus = onRequest(httpOptions, withCors((req, res) => { return require("./src/equipments").updateEquipmentStatus(req, res); })); exports.getEquipmentsPaginated = onRequest(httpOptions, withCors((req, res) => { return require("./src/equipments").getEquipmentsPaginated(req, res); })); exports.quickSearch = onRequest(httpOptions, withCors((req, res) => { return require("./src/equipments").quickSearch(req, res); })); // ============================================================================ // CONTAINERS // ============================================================================ exports.createContainer = onRequest(httpOptions, withCors((req, res) => { return require("./src/containers").createContainer(req, res); })); exports.updateContainer = onRequest(httpOptions, withCors((req, res) => { return require("./src/containers").updateContainer(req, res); })); exports.deleteContainer = onRequest(httpOptions, withCors((req, res) => { return require("./src/containers").deleteContainer(req, res); })); exports.getContainersByEquipment = onRequest(httpOptions, withCors((req, res) => { return require("./src/containers").getContainersByEquipment(req, res); })); exports.getContainersByIds = onRequest(httpOptions, withCors((req, res) => { return require("./src/containers").getContainersByIds(req, res); })); exports.addEquipmentToContainer = onRequest(httpOptions, withCors((req, res) => { return require("./src/containers").addEquipmentToContainer(req, res); })); exports.removeEquipmentFromContainer = onRequest(httpOptions, withCors((req, res) => { return require("./src/containers").removeEquipmentFromContainer(req, res); })); exports.getContainersPaginated = onRequest(httpOptions, withCors((req, res) => { return require("./src/containers").getContainersPaginated(req, res); })); // ============================================================================ // EVENTS // ============================================================================ exports.createEvent = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").createEvent(req, res); })); exports.updateEvent = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").updateEvent(req, res); })); exports.deleteEvent = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").deleteEvent(req, res); })); exports.updateEventEquipment = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").updateEventEquipment(req, res); })); exports.getEventsByEventType = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").getEventsByEventType(req, res); })); exports.getEvents = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").getEvents(req, res); })); exports.getEventsByMonth = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").getEventsByMonth(req, res); })); exports.searchEvents = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").searchEvents(req, res); })); exports.getEventWithDetails = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").getEventWithDetails(req, res); })); exports.validateEquipmentPreparation = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").validateEquipmentPreparation(req, res); })); exports.validateAllPreparation = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").validateAllPreparation(req, res); })); exports.validateEquipmentLoading = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").validateEquipmentLoading(req, res); })); exports.validateAllLoading = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").validateAllLoading(req, res); })); exports.validateEquipmentUnloading = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").validateEquipmentUnloading(req, res); })); exports.validateAllUnloading = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").validateAllUnloading(req, res); })); exports.validateEquipmentReturn = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").validateEquipmentReturn(req, res); })); exports.validateAllReturn = onRequest(httpOptions, withCors((req, res) => { return require("./src/events").validateAllReturn(req, res); })); // ============================================================================ // MAINTENANCES // ============================================================================ exports.createMaintenance = onRequest(httpOptions, withCors((req, res) => { return require("./src/maintenances").createMaintenance(req, res); })); exports.updateMaintenance = onRequest(httpOptions, withCors((req, res) => { return require("./src/maintenances").updateMaintenance(req, res); })); exports.getMaintenances = onRequest(httpOptions, withCors((req, res) => { return require("./src/maintenances").getMaintenances(req, res); })); exports.deleteMaintenance = onRequest(httpOptions, withCors((req, res) => { return require("./src/maintenances").deleteMaintenance(req, res); })); exports.checkUpcomingMaintenances = onRequest(httpOptions, withCors((req, res) => { return require("./src/maintenances").checkUpcomingMaintenances(req, res); })); exports.completeMaintenance = onRequest(httpOptions, withCors((req, res) => { return require("./src/maintenances").completeMaintenance(req, res); })); // ============================================================================ // OPTIONS & EVENT TYPES // ============================================================================ exports.createOption = onRequest(httpOptions, withCors((req, res) => { return require("./src/options").createOption(req, res); })); exports.updateOption = onRequest(httpOptions, withCors((req, res) => { return require("./src/options").updateOption(req, res); })); exports.deleteOption = onRequest(httpOptions, withCors((req, res) => { return require("./src/options").deleteOption(req, res); })); exports.getOptions = onRequest(httpOptions, withCors((req, res) => { return require("./src/options").getOptions(req, res); })); exports.getEventTypes = onRequest(httpOptions, withCors((req, res) => { return require("./src/options").getEventTypes(req, res); })); exports.createEventType = onRequest(httpOptions, withCors((req, res) => { return require("./src/options").createEventType(req, res); })); exports.updateEventType = onRequest(httpOptions, withCors((req, res) => { return require("./src/options").updateEventType(req, res); })); exports.deleteEventType = onRequest(httpOptions, withCors((req, res) => { return require("./src/options").deleteEventType(req, res); })); // ============================================================================ // USERS // ============================================================================ exports.createUser = onRequest(httpOptions, withCors((req, res) => { return require("./src/users").createUser(req, res); })); exports.createUserWithInvite = onRequest(httpOptions, withCors((req, res) => { return require("./src/users").createUserWithInvite(req, res); })); exports.updateUser = onRequest(httpOptions, withCors((req, res) => { return require("./src/users").updateUser(req, res); })); exports.deleteUser = onRequest(httpOptions, withCors((req, res) => { return require("./src/users").deleteUser(req, res); })); exports.getUsers = onRequest(httpOptions, withCors((req, res) => { return require("./src/users").getUsers(req, res); })); exports.getUser = onRequest(httpOptions, withCors((req, res) => { return require("./src/users").getUser(req, res); })); exports.getCurrentUser = onRequest(httpOptions, withCors((req, res) => { return require("./src/users").getCurrentUser(req, res); })); exports.getRoles = onRequest(httpOptions, withCors((req, res) => { return require("./src/users").getRoles(req, res); })); // ============================================================================ // ALERTS // ============================================================================ exports.getAlerts = onRequest(httpOptions, withCors((req, res) => { return require("./src/alerts").getAlerts(req, res); })); exports.markAlertAsRead = onRequest(httpOptions, withCors((req, res) => { return require("./src/alerts").markAlertAsRead(req, res); })); exports.deleteAlert = onRequest(httpOptions, withCors((req, res) => { return require("./src/alerts").deleteAlert(req, res); })); // ============================================================================ // AVAILABILITY // ============================================================================ exports.checkEquipmentAvailability = onRequest(httpOptions, withCors((req, res) => { return require("./src/availability").checkEquipmentAvailability(req, res); })); exports.checkContainerAvailability = onRequest(httpOptions, withCors((req, res) => { return require("./src/availability").checkContainerAvailability(req, res); })); exports.getConflictingEquipmentIds = onRequest(httpOptions, withCors((req, res) => { return require("./src/availability").getConflictingEquipmentIds(req, res); })); exports.findAlternativeEquipment = onRequest(httpOptions, withCors((req, res) => { return require("./src/availability").findAlternativeEquipment(req, res); })); exports.calculateEquipmentStatuses = onRequest(httpOptions, withCors((req, res) => { return require("./src/availability").calculateEquipmentStatuses(req, res); })); exports.getActiveEvents = onRequest(httpOptions, withCors((req, res) => { return require("./src/availability").getActiveEvents(req, res); })); // ============================================================================ // TEXT-TO-SPEECH // ============================================================================ exports.generateTTSV2 = onRequest(ttsHttpOptions, (req, res) => { return require("./src/tts").generateTTSV2(req, res); }); // ============================================================================ // AI // ============================================================================ exports.aiEquipmentProposal = onRequest(aiHttpOptions, withCors((req, res) => { return require("./aiEquipmentProposal").handleAiEquipmentProposal(req, res); })); // ============================================================================ // CALLABLE EMAIL & VALIDATION (LEGACY LAZY WRAPPERS) // ============================================================================ exports.sendAlertEmail = onCall({region: "europe-west9", cors: true}, (request) => { return require("./sendAlertEmail").sendAlertEmail(request); }); exports.createAlert = onCall({region: "europe-west9", cors: true}, (request) => { return require("./createAlert").createAlert(request); }); exports.processEquipmentValidation = onCall({region: "europe-west9", cors: true}, (request) => { return require("./processEquipmentValidation").processEquipmentValidation(request); }); // ============================================================================ // SCHEDULED FUNCTIONS // ============================================================================ exports.sendDailyDigest = onSchedule({ schedule: "0 8 * * *", timeZone: "Europe/Paris", region: "europe-west9", retryCount: 2, memory: "512MiB", }, async (context) => { logger.info("[Scheduler] Démarrage sendDailyDigest"); try { const {sendDailyDigest} = require("./sendDailyDigest"); await sendDailyDigest(); logger.info("[Scheduler] sendDailyDigest terminé avec succès"); } catch (error) { logger.error("[Scheduler] Erreur sendDailyDigest:", error); throw error; } }); // ============================================================================ // FIRESTORE TRIGGERS // ============================================================================ exports.onEventCreated = onDocumentCreated({ document: "events/{eventId}", region: "europe-west9", }, async (event) => { logger.info(`[onEventCreated] Événement créé: ${event.params.eventId}`); try { const eventData = event.data.data(); const eventId = event.params.eventId; await db.collection("alerts").add({ type: "EVENT_CREATED", severity: "INFO", message: `Nouvel événement créé : "${eventData.name}" le ${new Date(eventData.startDate?.toDate ? eventData.startDate.toDate() : eventData.startDate).toLocaleDateString("fr-FR")}`, eventId: eventId, eventName: eventData.name, eventDate: eventData.startDate, createdAt: admin.firestore.FieldValue.serverTimestamp(), isRead: false, metadata: { eventId: eventId, eventName: eventData.name, eventDate: eventData.startDate, }, assignedTo: [], }); logger.info(`[onEventCreated] Alerte créée pour événement ${eventId}`); } catch (error) { logger.error("[onEventCreated] Erreur:", error); } }); exports.onEventUpdated = onDocumentUpdated({ document: "events/{eventId}", region: "europe-west9", }, async (event) => { const before = event.data.before.data(); const after = event.data.after.data(); const eventId = event.params.eventId; try { const workforceBefore = before.workforce || []; const workforceAfter = after.workforce || []; const newMembers = workforceAfter.filter((afterMember) => { return !workforceBefore.some((beforeMember) => beforeMember.userId === afterMember.userId, ); }); if (newMembers.length > 0) { logger.info(`[onEventUpdated] ${newMembers.length} nouveaux membres ajoutés à ${eventId}`); for (const member of newMembers) { await db.collection("alerts").add({ type: "WORKFORCE_ADDED", severity: "INFO", message: `Vous avez été ajouté(e) à l'événement "${after.name}" le ${new Date(after.startDate?.toDate ? after.startDate.toDate() : after.startDate).toLocaleDateString("fr-FR")}`, eventId: eventId, eventName: after.name, eventDate: after.startDate, createdAt: admin.firestore.FieldValue.serverTimestamp(), isRead: false, metadata: { eventId: eventId, eventName: after.name, eventDate: after.startDate, }, assignedTo: [member.userId], }); logger.info(`[onEventUpdated] Alerte créée pour ${member.userId}`); } } } catch (error) { logger.error("[onEventUpdated] Erreur:", error); } }); exports.onAlertCreated = onDocumentCreated({ document: "alerts/{alertId}", region: "europe-west9", }, async (event) => { const alertId = event.params.alertId; const alertData = event.data.data(); logger.info(`[onAlertCreated] Nouvelle alerte: ${alertId} (${alertData.severity})`); try { if (alertData.severity === "CRITICAL" && !alertData.emailSent) { const userIds = alertData.assignedTo || []; if (userIds.length > 0) { logger.info(`[onAlertCreated] Envoi email immédiat à ${userIds.length} utilisateurs`); await db.collection("alerts").doc(alertId).update({ pendingEmailSend: true, }); logger.info(`[onAlertCreated] Alerte marquée pour envoi email`); } } } catch (error) { logger.error("[onAlertCreated] Erreur:", error); } });