/** * 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(); // ============================================================================ // STORAGE - Move Event File // ============================================================================ exports.moveEventFileV2 = onRequest({ cors: true }, 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({ cors: true }, 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({ cors: true }, 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({ cors: true }, 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({ cors: true }, 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({ cors: true }, 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({ cors: true }, 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({ cors: true }, 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 }); } }); // ============================================================================ // EVENTS - CRUD // ============================================================================ // Créer un événement exports.createEvent = onRequest({ cors: true }, 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({ cors: true }, 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 { eventId, data } = req.body.data; if (!eventId) { res.status(400).json({ error: 'Event ID is required' }); 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({ cors: true }, 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({ cors: true }, 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({ cors: true }, 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({ cors: true }, 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({ cors: true }, 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({ cors: true }, 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({ cors: true }, 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({ cors: true }, 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({ cors: true }, 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 }); } });