Files
EM2_ERP/em2rp/functions/index.js

676 lines
21 KiB
JavaScript

/**
* 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 });
}
});