/** * EM2RP Cloud Functions * Architecture backend sécurisée avec authentification et permissions */ const { onRequest } = require("firebase-functions/v2/https"); const logger = require("firebase-functions/logger"); const admin = require('firebase-admin'); const { Storage } = require('@google-cloud/storage'); // Utilitaires const auth = require('./utils/auth'); const helpers = require('./utils/helpers'); // Initialisation admin.initializeApp(); const storage = new Storage(); const db = admin.firestore(); // Configuration commune pour toutes les fonctions HTTP const httpOptions = { cors: true, invoker: 'public', // Permet les invocations non authentifiées (l'auth est gérée par notre token Firebase) // Version: 2.0 - Ajout de l'invoker public pour résoudre les problèmes CORS }; // ============================================================================ // CORS Middleware // ============================================================================ const setCorsHeaders = (res, req) => { // Permettre toutes les origines en développement/production const origin = req.headers.origin || req.headers.referer || '*'; res.set('Access-Control-Allow-Origin', 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'); }; // Wrapper pour les fonctions avec CORS const withCors = (handler) => { return async (req, res) => { // Définir les headers CORS pour toutes les requêtes setCorsHeaders(res, req); // Gérer les requêtes preflight OPTIONS if (req.method === 'OPTIONS') { res.status(204).send(''); return; } // Exécuter le handler try { await handler(req, res); } catch (error) { logger.error("Unhandled error:", error); if (!res.headersSent) { res.status(500).json({ error: error.message }); } } }; }; // ============================================================================ // STORAGE - Move Event File // ============================================================================ exports.moveEventFileV2 = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const { sourcePath, destinationPath } = req.body.data || {}; if (!sourcePath || !destinationPath) { res.status(400).json({ error: 'Source and destination paths are required.' }); return; } const bucketName = admin.storage().bucket().name; const bucket = storage.bucket(bucketName); await bucket.file(sourcePath).copy(bucket.file(destinationPath)); await bucket.file(sourcePath).delete(); const [url] = await bucket.file(destinationPath).getSignedUrl({ action: 'read', expires: '03-01-2500', }); res.status(200).json({ url }); } catch (error) { logger.error("Error moving file:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // EQUIPMENT - CRUD // ============================================================================ // Créer un équipement (admin ou manage_equipment) exports.createEquipment = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); if (!hasAccess) { res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' }); return; } const equipmentData = req.body.data; const equipmentId = equipmentData.id; if (!equipmentId) { res.status(400).json({ error: 'Equipment ID is required' }); return; } // Vérifier unicité de l'ID const existingDoc = await db.collection('equipments').doc(equipmentId).get(); if (existingDoc.exists) { res.status(409).json({ error: 'Equipment ID already exists' }); return; } // Convertir les timestamps const dataToSave = helpers.deserializeTimestamps(equipmentData, [ 'createdAt', 'updatedAt', 'purchaseDate', 'lastMaintenanceDate', 'nextMaintenanceDate' ]); await db.collection('equipments').doc(equipmentId).set(dataToSave); res.status(201).json({ id: equipmentId, message: 'Equipment created successfully' }); } catch (error) { logger.error("Error creating equipment:", error); res.status(500).json({ error: error.message }); } })); // Mettre à jour un équipement exports.updateEquipment = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); if (!hasAccess) { res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' }); return; } const { equipmentId, data } = req.body.data; if (!equipmentId) { res.status(400).json({ error: 'Equipment ID is required' }); return; } // Empêcher la modification de l'ID delete data.id; // Ajouter updatedAt data.updatedAt = admin.firestore.Timestamp.now(); const dataToSave = helpers.deserializeTimestamps(data, [ 'purchaseDate', 'lastMaintenanceDate', 'nextMaintenanceDate' ]); await db.collection('equipments').doc(equipmentId).update(dataToSave); res.status(200).json({ message: 'Equipment updated successfully' }); } catch (error) { logger.error("Error updating equipment:", error); res.status(500).json({ error: error.message }); } })); // Supprimer un équipement exports.deleteEquipment = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); if (!hasAccess) { res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' }); return; } const { equipmentId } = req.body.data; if (!equipmentId) { res.status(400).json({ error: 'Equipment ID is required' }); return; } // Vérifier si l'équipement est utilisé dans des événements actifs const eventsSnapshot = await db.collection('events') .where('status', '!=', 'CANCELLED') .get(); for (const eventDoc of eventsSnapshot.docs) { const eventData = eventDoc.data(); const assignedEquipment = eventData.assignedEquipment || []; if (assignedEquipment.some(eq => eq.equipmentId === equipmentId)) { res.status(409).json({ error: 'Cannot delete equipment: it is assigned to active events', eventId: eventDoc.id }); return; } } await db.collection('equipments').doc(equipmentId).delete(); res.status(200).json({ message: 'Equipment deleted successfully' }); } catch (error) { logger.error("Error deleting equipment:", error); res.status(500).json({ error: error.message }); } })); // Récupérer un équipement par ID exports.getEquipment = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasViewAccess = await auth.hasPermission(decodedToken.uid, 'view_equipment'); const hasManageAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); if (!hasViewAccess && !hasManageAccess) { res.status(403).json({ error: 'Forbidden: Requires view_equipment permission' }); return; } const { equipmentId } = req.body.data || req.query; if (!equipmentId) { res.status(400).json({ error: 'Equipment ID is required' }); return; } const doc = await db.collection('equipments').doc(equipmentId).get(); if (!doc.exists) { res.status(404).json({ error: 'Equipment not found' }); return; } let data = { id: doc.id, ...doc.data() }; data = helpers.serializeTimestamps(data); data = helpers.serializeReferences(data); // Masquer les prix si pas de permission manage_equipment data = helpers.maskSensitiveFields(data, hasManageAccess); res.status(200).json({ equipment: data }); } catch (error) { logger.error("Error getting equipment:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // CONTAINERS - CRUD // ============================================================================ // Créer un container exports.createContainer = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); if (!hasAccess) { res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' }); return; } const containerData = req.body.data; const containerId = containerData.id; if (!containerId) { res.status(400).json({ error: 'Container ID is required' }); return; } const existingDoc = await db.collection('containers').doc(containerId).get(); if (existingDoc.exists) { res.status(409).json({ error: 'Container ID already exists' }); return; } const dataToSave = helpers.deserializeTimestamps(containerData, ['createdAt', 'updatedAt']); await db.collection('containers').doc(containerId).set(dataToSave); res.status(201).json({ id: containerId, message: 'Container created successfully' }); } catch (error) { logger.error("Error creating container:", error); res.status(500).json({ error: error.message }); } })); // Mettre à jour un container exports.updateContainer = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); if (!hasAccess) { res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' }); return; } const { containerId, data } = req.body.data; if (!containerId) { res.status(400).json({ error: 'Container ID is required' }); return; } delete data.id; data.updatedAt = admin.firestore.Timestamp.now(); await db.collection('containers').doc(containerId).update(data); res.status(200).json({ message: 'Container updated successfully' }); } catch (error) { logger.error("Error updating container:", error); res.status(500).json({ error: error.message }); } })); // Supprimer un container exports.deleteContainer = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); if (!hasAccess) { res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' }); return; } const { containerId } = req.body.data; if (!containerId) { res.status(400).json({ error: 'Container ID is required' }); return; } await db.collection('containers').doc(containerId).delete(); res.status(200).json({ message: 'Container deleted successfully' }); } catch (error) { logger.error("Error deleting container:", error); res.status(500).json({ error: error.message }); } })); // Récupérer les containers contenant un équipement exports.getContainersByEquipment = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasViewAccess = await auth.hasPermission(decodedToken.uid, 'view_equipment'); const hasManageAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); if (!hasViewAccess && !hasManageAccess) { res.status(403).json({ error: 'Forbidden: Requires view_equipment permission' }); return; } const { equipmentId } = req.body.data || req.query; if (!equipmentId) { res.status(400).json({ error: 'equipmentId is required' }); return; } // Récupérer tous les containers qui contiennent cet équipement const containersSnapshot = await db.collection('containers') .where('equipmentIds', 'array-contains', equipmentId) .get(); const containers = []; containersSnapshot.forEach(doc => { let data = { id: doc.id, ...doc.data() }; data = helpers.serializeTimestamps(data); data = helpers.serializeReferences(data); containers.push(data); }); res.status(200).json({ containers }); } catch (error) { logger.error("Error getting containers by equipment:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // EVENTS - CRUD // ============================================================================ // Créer un événement exports.createEvent = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'edit_event'); if (!hasAccess) { res.status(403).json({ error: 'Forbidden: Requires edit_event permission' }); return; } const eventData = req.body.data; const dataToSave = helpers.deserializeTimestamps(eventData, [ 'startDateTime', 'endDateTime', 'createdAt', 'updatedAt' ]); const docRef = await db.collection('events').add(dataToSave); res.status(201).json({ id: docRef.id, message: 'Event created successfully' }); } catch (error) { logger.error("Error creating event:", error); res.status(500).json({ error: error.message }); } })); // Mettre à jour un événement exports.updateEvent = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'edit_event'); if (!hasAccess) { res.status(403).json({ error: 'Forbidden: Requires edit_event permission' }); return; } const requestData = req.body.data; const eventId = requestData.eventId; if (!eventId) { res.status(400).json({ error: 'Event ID is required' }); return; } // Extraire eventId et préparer les données à sauvegarder const { eventId: _, ...data } = requestData; if (!data || Object.keys(data).length === 0) { res.status(400).json({ error: 'No data to update' }); return; } delete data.id; data.updatedAt = admin.firestore.Timestamp.now(); const dataToSave = helpers.deserializeTimestamps(data, [ 'startDateTime', 'endDateTime' ]); await db.collection('events').doc(eventId).update(dataToSave); res.status(200).json({ message: 'Event updated successfully' }); } catch (error) { logger.error("Error updating event:", error); res.status(500).json({ error: error.message }); } })); // Supprimer un événement exports.deleteEvent = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'delete_event'); if (!hasAccess) { res.status(403).json({ error: 'Forbidden: Requires delete_event permission' }); return; } const { eventId } = req.body.data; if (!eventId) { res.status(400).json({ error: 'Event ID is required' }); return; } await db.collection('events').doc(eventId).delete(); res.status(200).json({ message: 'Event deleted successfully' }); } catch (error) { logger.error("Error deleting event:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // MAINTENANCES - CRUD // ============================================================================ // Créer une maintenance exports.createMaintenance = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_maintenances'); if (!hasAccess) { res.status(403).json({ error: 'Forbidden: Requires manage_maintenances permission' }); return; } const maintenanceData = req.body.data; const dataToSave = helpers.deserializeTimestamps(maintenanceData, [ 'scheduledDate', 'completedDate', 'createdAt', 'updatedAt' ]); const docRef = await db.collection('maintenances').add(dataToSave); res.status(201).json({ id: docRef.id, message: 'Maintenance created successfully' }); } catch (error) { logger.error("Error creating maintenance:", error); res.status(500).json({ error: error.message }); } })); // Mettre à jour une maintenance exports.updateMaintenance = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_maintenances'); if (!hasAccess) { res.status(403).json({ error: 'Forbidden: Requires manage_maintenances permission' }); return; } const { maintenanceId, data } = req.body.data; if (!maintenanceId) { res.status(400).json({ error: 'Maintenance ID is required' }); return; } delete data.id; data.updatedAt = admin.firestore.Timestamp.now(); const dataToSave = helpers.deserializeTimestamps(data, [ 'scheduledDate', 'completedDate' ]); await db.collection('maintenances').doc(maintenanceId).update(dataToSave); res.status(200).json({ message: 'Maintenance updated successfully' }); } catch (error) { logger.error("Error updating maintenance:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // OPTIONS - CRUD // ============================================================================ // Créer une option exports.createOption = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const isAdminUser = await auth.isAdmin(decodedToken.uid); if (!isAdminUser) { res.status(403).json({ error: 'Forbidden: Admin access required' }); return; } const optionData = req.body.data; const optionId = optionData.id; if (!optionId) { res.status(400).json({ error: 'Option ID is required' }); return; } await db.collection('options').doc(optionId).set(optionData); res.status(201).json({ id: optionId, message: 'Option created successfully' }); } catch (error) { logger.error("Error creating option:", error); res.status(500).json({ error: error.message }); } })); // Mettre à jour une option exports.updateOption = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const isAdminUser = await auth.isAdmin(decodedToken.uid); if (!isAdminUser) { res.status(403).json({ error: 'Forbidden: Admin access required' }); return; } const { optionId, data } = req.body.data; if (!optionId) { res.status(400).json({ error: 'Option ID is required' }); return; } delete data.id; await db.collection('options').doc(optionId).update(data); res.status(200).json({ message: 'Option updated successfully' }); } catch (error) { logger.error("Error updating option:", error); res.status(500).json({ error: error.message }); } })); // Supprimer une option exports.deleteOption = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const isAdminUser = await auth.isAdmin(decodedToken.uid); if (!isAdminUser) { res.status(403).json({ error: 'Forbidden: Admin access required' }); return; } const { optionId } = req.body.data; if (!optionId) { res.status(400).json({ error: 'Option ID is required' }); return; } await db.collection('options').doc(optionId).delete(); res.status(200).json({ message: 'Option deleted successfully' }); } catch (error) { logger.error("Error deleting option:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // USERS - CRUD // ============================================================================ // Créer un utilisateur exports.createUser = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const isAdminUser = await auth.isAdmin(decodedToken.uid); if (!isAdminUser) { res.status(403).json({ error: 'Forbidden: Admin access required' }); return; } const userData = req.body.data; const userId = userData.uid; if (!userId) { res.status(400).json({ error: 'User ID is required' }); return; } await db.collection('users').doc(userId).set(userData); res.status(201).json({ id: userId, message: 'User created successfully' }); } catch (error) { logger.error("Error creating user:", error); res.status(500).json({ error: error.message }); } })); // Mettre à jour un utilisateur exports.updateUser = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const { userId, data } = req.body.data; if (!userId) { res.status(400).json({ error: 'User ID is required' }); return; } // Vérifier si l'utilisateur met à jour son propre profil ou est admin const isOwnProfile = decodedToken.uid === userId; const isAdminUser = await auth.isAdmin(decodedToken.uid); const hasEditPermission = await auth.hasPermission(decodedToken.uid, 'edit_user'); if (!isOwnProfile && !isAdminUser && !hasEditPermission) { res.status(403).json({ error: 'Forbidden: Cannot edit other users' }); return; } // Si mise à jour propre profil, limiter les champs modifiables if (isOwnProfile && !isAdminUser) { const allowedFields = ['firstName', 'lastName', 'phoneNumber', 'profilePhotoUrl']; const filteredData = {}; for (const field of allowedFields) { if (data[field] !== undefined) { filteredData[field] = data[field]; } } await db.collection('users').doc(userId).update(filteredData); } else { delete data.uid; await db.collection('users').doc(userId).update(data); } res.status(200).json({ message: 'User updated successfully' }); } catch (error) { logger.error("Error updating user:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // EQUIPMENT STATUS - Batch Update // ============================================================================ // Mettre à jour le statut de plusieurs équipements (pour préparation/retour) exports.updateEquipmentStatus = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const { eventId, updates } = req.body.data; if (!eventId || !updates || !Array.isArray(updates)) { res.status(400).json({ error: 'Event ID and updates array are required' }); return; } // Vérifier que l'utilisateur est assigné à l'événement ou est admin const isAssigned = await auth.isAssignedToEvent(decodedToken.uid, eventId); const isAdminUser = await auth.isAdmin(decodedToken.uid); if (!isAssigned && !isAdminUser) { res.status(403).json({ error: 'Forbidden: Not assigned to this event' }); return; } // Batch update const batch = db.batch(); for (const update of updates) { const { equipmentId, status } = update; if (equipmentId && status) { const equipmentRef = db.collection('equipments').doc(equipmentId); batch.update(equipmentRef, { status }); } } await batch.commit(); res.status(200).json({ message: 'Equipment statuses updated successfully' }); } catch (error) { logger.error("Error updating equipment statuses:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // OPTIONS - Read (public pour utilisateurs authentifiés) // ============================================================================ exports.getOptions = onRequest(httpOptions, withCors(async (req, res) => { try { await auth.authenticateUser(req); // Juste vérifier l'auth const snapshot = await db.collection('options').get(); const options = snapshot.docs.map(doc => ({ id: doc.id, ...helpers.serializeTimestamps(doc.data()) })); res.status(200).json({ options }); } catch (error) { logger.error("Error fetching options:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // EVENT TYPES - Read (public pour utilisateurs authentifiés) // ============================================================================ exports.getEventTypes = onRequest(httpOptions, withCors(async (req, res) => { try { await auth.authenticateUser(req); // Juste vérifier l'auth const snapshot = await db.collection('eventTypes').get(); const eventTypes = snapshot.docs.map(doc => ({ id: doc.id, ...helpers.serializeTimestamps(doc.data()) })); res.status(200).json({ eventTypes }); } catch (error) { logger.error("Error fetching event types:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // ROLES - Read (public pour utilisateurs authentifiés) // ============================================================================ exports.getRoles = onRequest(httpOptions, withCors(async (req, res) => { try { await auth.authenticateUser(req); // Juste vérifier l'auth const snapshot = await db.collection('roles').get(); const roles = snapshot.docs.map(doc => ({ id: doc.id, ...helpers.serializeTimestamps(doc.data()) })); res.status(200).json({ roles }); } catch (error) { logger.error("Error fetching roles:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // EVENT EQUIPMENT - Update equipment status and quantities // ============================================================================ exports.updateEventEquipment = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const { eventId, assignedEquipment, preparationStatus, loadingStatus, unloadingStatus, returnStatus } = req.body.data; if (!eventId) { res.status(400).json({ error: 'Event ID is required' }); return; } // Vérifier les permissions const eventDoc = await db.collection('events').doc(eventId).get(); if (!eventDoc.exists) { res.status(404).json({ error: 'Event not found' }); return; } const eventData = eventDoc.data(); const isAdminUser = await auth.hasPermission(decodedToken.uid, 'edit_event'); const isAssigned = eventData.workforce?.some(ref => ref.path.endsWith(decodedToken.uid)); if (!isAssigned && !isAdminUser) { res.status(403).json({ error: 'Forbidden: Not assigned to this event' }); return; } // Préparer les données à mettre à jour const updateData = {}; if (assignedEquipment) { // Convertir les timestamps dans assignedEquipment updateData.assignedEquipment = assignedEquipment.map(eq => helpers.deserializeTimestamps(eq, []) ); } if (preparationStatus) updateData.preparationStatus = preparationStatus; if (loadingStatus) updateData.loadingStatus = loadingStatus; if (unloadingStatus) updateData.unloadingStatus = unloadingStatus; if (returnStatus) updateData.returnStatus = returnStatus; // Mettre à jour l'événement await db.collection('events').doc(eventId).update(updateData); res.status(200).json({ message: 'Event equipment updated successfully' }); } catch (error) { logger.error("Error updating event equipment:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // EQUIPMENT STATUS - Update individual equipment status // ============================================================================ exports.updateEquipmentStatusOnly = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const { equipmentId, status, availableQuantity } = req.body.data; if (!equipmentId) { res.status(400).json({ error: 'Equipment ID is required' }); return; } // Vérifier les permissions const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); if (!hasAccess) { res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' }); return; } const updateData = { updatedAt: admin.firestore.Timestamp.now() }; if (status) updateData.status = status; if (availableQuantity !== undefined) updateData.availableQuantity = availableQuantity; await db.collection('equipments').doc(equipmentId).update(updateData); res.status(200).json({ message: 'Equipment status updated successfully' }); } catch (error) { logger.error("Error updating equipment status:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // EVENT TYPES - CRUD Operations // ============================================================================ // Récupérer les événements utilisant un type d'événement exports.getEventsByEventType = onRequest(httpOptions, withCors(async (req, res) => { try { await auth.authenticateUser(req); const { eventTypeId } = req.body.data; if (!eventTypeId) { res.status(400).json({ error: 'Event type ID is required' }); return; } const eventsSnapshot = await db.collection('events') .where('eventTypeId', '==', eventTypeId) .get(); const events = eventsSnapshot.docs.map(doc => ({ id: doc.id, name: doc.data().name, startDateTime: doc.data().StartDateTime, })); res.status(200).json({ events }); } catch (error) { logger.error("Error fetching events by type:", error); res.status(500).json({ error: error.message }); } })); // Créer un type d'événement (admin uniquement) exports.createEventType = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const isAdmin = await auth.hasPermission(decodedToken.uid, 'edit_data'); if (!isAdmin) { res.status(403).json({ error: 'Forbidden: Admin permission required' }); return; } const { name, defaultPrice } = req.body.data; if (!name || defaultPrice === undefined) { res.status(400).json({ error: 'Name and defaultPrice are required' }); return; } // Vérifier l'unicité du nom const existingSnapshot = await db.collection('eventTypes') .where('name', '==', name) .get(); if (!existingSnapshot.empty) { res.status(409).json({ error: 'Event type name already exists' }); return; } const eventTypeData = { name, defaultPrice, createdAt: admin.firestore.Timestamp.now(), }; const docRef = await db.collection('eventTypes').add(eventTypeData); res.status(201).json({ id: docRef.id, message: 'Event type created successfully' }); } catch (error) { logger.error("Error creating event type:", error); res.status(500).json({ error: error.message }); } })); // Mettre à jour un type d'événement (admin uniquement) exports.updateEventType = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const isAdmin = await auth.hasPermission(decodedToken.uid, 'edit_data'); if (!isAdmin) { res.status(403).json({ error: 'Forbidden: Admin permission required' }); return; } const { eventTypeId, name, defaultPrice } = req.body.data; if (!eventTypeId) { res.status(400).json({ error: 'Event type ID is required' }); return; } // Vérifier que le document existe const docRef = db.collection('eventTypes').doc(eventTypeId); const doc = await docRef.get(); if (!doc.exists) { res.status(404).json({ error: 'Event type not found' }); return; } // Vérifier l'unicité du nom (sauf pour le document actuel) if (name) { const existingSnapshot = await db.collection('eventTypes') .where('name', '==', name) .get(); const hasDuplicate = existingSnapshot.docs.some(d => d.id !== eventTypeId); if (hasDuplicate) { res.status(409).json({ error: 'Event type name already exists' }); return; } } const updateData = {}; if (name) updateData.name = name; if (defaultPrice !== undefined) updateData.defaultPrice = defaultPrice; await docRef.update(updateData); res.status(200).json({ message: 'Event type updated successfully' }); } catch (error) { logger.error("Error updating event type:", error); res.status(500).json({ error: error.message }); } })); // Supprimer un type d'événement (admin uniquement) exports.deleteEventType = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const isAdmin = await auth.hasPermission(decodedToken.uid, 'edit_data'); if (!isAdmin) { res.status(403).json({ error: 'Forbidden: Admin permission required' }); return; } const { eventTypeId } = req.body.data; if (!eventTypeId) { res.status(400).json({ error: 'Event type ID is required' }); return; } // Vérifier qu'aucun événement futur n'utilise ce type const eventsSnapshot = await db.collection('events') .where('eventTypeId', '==', eventTypeId) .get(); const now = admin.firestore.Timestamp.now(); const futureEvents = eventsSnapshot.docs.filter(doc => { const startDate = doc.data().StartDateTime; return startDate && startDate > now; }); if (futureEvents.length > 0) { res.status(409).json({ error: 'Cannot delete event type with future events', futureEventsCount: futureEvents.length }); return; } await db.collection('eventTypes').doc(eventTypeId).delete(); res.status(200).json({ message: 'Event type deleted successfully' }); } catch (error) { logger.error("Error deleting event type:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // EVENTS - Read with permissions // ============================================================================ exports.getEvents = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const { userId } = req.body.data || {}; // Vérifier si l'utilisateur peut voir tous les événements const canViewAll = await auth.hasPermission(decodedToken.uid, 'view_all_events'); let eventsSnapshot; if (canViewAll) { // Admin : tous les événements eventsSnapshot = await db.collection('events').get(); } else { // Utilisateur normal : seulement ses événements assignés const userRef = db.collection('users').doc(userId || decodedToken.uid); eventsSnapshot = await db.collection('events') .where('workforce', 'array-contains', userRef) .get(); } // Collecter tous les UIDs utilisateurs uniques const userIdsSet = new Set(); eventsSnapshot.docs.forEach(doc => { const data = doc.data(); if (data.workforce && Array.isArray(data.workforce)) { data.workforce.forEach(userRef => { if (userRef && userRef.id) { userIdsSet.add(userRef.id); } else if (typeof userRef === 'string' && userRef.startsWith('users/')) { userIdsSet.add(userRef.split('/')[1]); } }); } }); // Récupérer tous les utilisateurs en une seule fois const usersMap = {}; if (userIdsSet.size > 0) { const userIds = Array.from(userIdsSet); // Récupérer par batch (Firestore limite à 10 par requête 'in') const batchSize = 10; for (let i = 0; i < userIds.length; i += batchSize) { const batch = userIds.slice(i, i + batchSize); const usersSnapshot = await db.collection('users') .where(admin.firestore.FieldPath.documentId(), 'in', batch) .get(); usersSnapshot.docs.forEach(userDoc => { const userData = userDoc.data(); // Stocker uniquement les données publiques usersMap[userDoc.id] = { uid: userDoc.id, firstName: userData.firstName || '', lastName: userData.lastName || '', email: userData.email || '', phoneNumber: userData.phoneNumber || '', profilePhotoUrl: userData.profilePhotoUrl || '', }; }); } } // Sérialiser les événements avec workforce comme liste d'UIDs const events = eventsSnapshot.docs.map(doc => { const data = doc.data(); // Convertir workforce en liste d'UIDs let workforceUids = []; if (data.workforce && Array.isArray(data.workforce)) { workforceUids = data.workforce.map(userRef => { if (userRef && userRef.id) { return userRef.id; } else if (typeof userRef === 'string' && userRef.startsWith('users/')) { return userRef.split('/')[1]; } return null; }).filter(uid => uid !== null); } return { id: doc.id, ...helpers.serializeTimestamps(data), workforce: workforceUids, // Liste d'UIDs au lieu de DocumentReference }; }); // Retourner events + users map res.status(200).json({ events, users: usersMap // Map UID -> données utilisateur }); } catch (error) { logger.error("Error fetching events:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // EQUIPMENTS - Read with permissions // ============================================================================ exports.getEquipments = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); // Vérifier les permissions const canManage = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); const canView = await auth.hasPermission(decodedToken.uid, 'view_equipment'); if (!canManage && !canView) { res.status(403).json({ error: 'Forbidden: Requires equipment permissions' }); return; } const snapshot = await db.collection('equipments').get(); const equipments = snapshot.docs.map(doc => { const data = doc.data(); // Masquer les prix si l'utilisateur n'a pas manage_equipment if (!canManage) { delete data.purchasePrice; delete data.rentalPrice; } return { id: doc.id, ...helpers.serializeTimestamps(data, ['purchaseDate', 'nextMaintenanceDate', 'lastMaintenanceDate', 'createdAt', 'updatedAt']) }; }); res.status(200).json({ equipments }); } catch (error) { logger.error("Error fetching equipments:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // CONTAINERS - Read with permissions // ============================================================================ exports.getContainers = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); // Vérifier les permissions const canView = await auth.hasPermission(decodedToken.uid, 'view_equipment'); if (!canView) { res.status(403).json({ error: 'Forbidden: Requires equipment permissions' }); return; } const snapshot = await db.collection('containers').get(); const containers = snapshot.docs.map(doc => { const data = doc.data(); return { id: doc.id, ...helpers.serializeTimestamps(data, ['createdAt', 'updatedAt']) }; }); res.status(200).json({ containers }); } catch (error) { logger.error("Error fetching containers:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // MAINTENANCES - Read with permissions // ============================================================================ exports.getMaintenances = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); const { equipmentId } = req.body.data || {}; // Vérifier les permissions const canView = await auth.hasPermission(decodedToken.uid, 'view_equipment'); if (!canView) { res.status(403).json({ error: 'Forbidden: Requires equipment permissions' }); return; } let query = db.collection('maintenances'); // Filtrer par équipement si spécifié if (equipmentId) { query = query.where('equipmentIds', 'array-contains', equipmentId); } const snapshot = await query.get(); const maintenances = snapshot.docs.map(doc => { const data = doc.data(); return { id: doc.id, ...helpers.serializeTimestamps(data, ['scheduledDate', 'completedDate', 'createdAt', 'updatedAt']) }; }); res.status(200).json({ maintenances }); } catch (error) { logger.error("Error fetching maintenances:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // ALERTS - Read with permissions // ============================================================================ exports.getAlerts = onRequest(httpOptions, withCors(async (req, res) => { try { await auth.authenticateUser(req); const snapshot = await db.collection('alerts') .orderBy('createdAt', 'desc') .limit(100) .get(); const alerts = snapshot.docs.map(doc => { const data = doc.data(); return { id: doc.id, ...helpers.serializeTimestamps(data, ['createdAt']) }; }); res.status(200).json({ alerts }); } catch (error) { logger.error("Error fetching alerts:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // USERS - Read with permissions // ============================================================================ exports.getUsers = onRequest(httpOptions, withCors(async (req, res) => { try { const decodedToken = await auth.authenticateUser(req); // Vérifier les permissions const canViewAll = await auth.hasPermission(decodedToken.uid, 'view_all_users'); if (!canViewAll) { // Si pas admin, ne retourner que l'utilisateur lui-même const userDoc = await db.collection('users').doc(decodedToken.uid).get(); if (!userDoc.exists) { res.status(404).json({ error: 'User not found' }); return; } const userData = userDoc.data(); res.status(200).json({ users: [{ id: userDoc.id, ...helpers.serializeTimestamps(userData) }] }); return; } // Admin : tous les utilisateurs const snapshot = await db.collection('users').get(); const users = snapshot.docs.map(doc => ({ id: doc.id, ...helpers.serializeTimestamps(doc.data()) })); res.status(200).json({ users }); } catch (error) { logger.error("Error fetching users:", error); res.status(500).json({ error: error.message }); } })); // ============================================================================ // CONTAINERS - Récupération par équipement // ============================================================================ /** * Récupère tous les containers contenant un équipement spécifique * Accessible à tous les utilisateurs authentifiés */ exports.getContainersByEquipment = onRequest(httpOptions, withCors(async (req, res) => { try { // Vérifier l'authentification const user = await auth.authenticateUser(req); const equipmentId = req.body.data?.equipmentId; if (!equipmentId) { res.status(400).json({ error: 'equipmentId is required' }); return; } logger.info(`Fetching containers for equipment: ${equipmentId}`); // Requête pour trouver tous les containers contenant cet équipement const containersSnapshot = await db.collection('containers') .where('equipmentIds', 'array-contains', equipmentId) .get(); const containers = containersSnapshot.docs.map(doc => ({ id: doc.id, ...helpers.serializeTimestamps(doc.data()) })); logger.info(`Found ${containers.length} container(s) for equipment ${equipmentId}`); res.status(200).json({ containers, count: containers.length }); } catch (error) { logger.error("Error fetching containers by equipment:", error); res.status(500).json({ error: error.message }); } }));