feat: ajout de la configuration des émulateurs Firebase et mise à jour des services pour utiliser le backend sécurisé
This commit is contained in:
@@ -1,71 +1,675 @@
|
||||
/**
|
||||
* Import function triggers from their respective submodules:
|
||||
*
|
||||
* const {onCall} = require("firebase-functions/v2/https");
|
||||
* const {onDocumentWritten} = require("firebase-functions/v2/firestore");
|
||||
*
|
||||
* See a full list of supported triggers at https://firebase.google.com/docs/functions
|
||||
* EM2RP Cloud Functions
|
||||
* Architecture backend sécurisée avec authentification et permissions
|
||||
*/
|
||||
|
||||
const {onRequest} = require("firebase-functions/v2/https");
|
||||
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();
|
||||
|
||||
// Create and deploy your first functions
|
||||
// https://firebase.google.com/docs/functions/get-started
|
||||
// ============================================================================
|
||||
// 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 || {};
|
||||
|
||||
// exports.helloWorld = onRequest((request, response) => {
|
||||
// logger.info("Hello logs!", {structuredData: true});
|
||||
// response.send("Hello from Firebase!");
|
||||
// });
|
||||
|
||||
|
||||
// Nouvelle version HTTP sécurisée
|
||||
exports.moveEventFileV2 = onRequest({cors: true}, async (req, res) => {
|
||||
// La gestion CORS est maintenant gérée par l'option {cors: true}
|
||||
// La vérification pour les requêtes OPTIONS n'est plus nécessaire
|
||||
|
||||
// Vérification du token Firebase dans l'en-tête Authorization
|
||||
let uid = null;
|
||||
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
|
||||
const idToken = req.headers.authorization.split('Bearer ')[1];
|
||||
try {
|
||||
const decodedToken = await admin.auth().verifyIdToken(idToken);
|
||||
uid = decodedToken.uid;
|
||||
} catch (e) {
|
||||
logger.error("Error while verifying Firebase ID token:", e);
|
||||
res.status(401).json({ error: 'Unauthorized: Invalid token' });
|
||||
if (!sourcePath || !destinationPath) {
|
||||
res.status(400).json({ error: 'Source and destination paths are required.' });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
logger.warn("No Firebase ID token was passed as a Bearer token in the Authorization header.");
|
||||
res.status(401).json({ error: 'Unauthorized: No token provided' });
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const bucketName = admin.storage().bucket().name;
|
||||
const bucket = storage.bucket(bucketName);
|
||||
|
||||
try {
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
282
em2rp/functions/package-lock.json
generated
282
em2rp/functions/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
54
em2rp/functions/test_functions.js
Normal file
54
em2rp/functions/test_functions.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Test rapide des Cloud Functions
|
||||
* Vérifie que toutes les fonctions sont exportées correctement
|
||||
*/
|
||||
|
||||
const functions = require('./index');
|
||||
|
||||
console.log('🧪 Test des Cloud Functions\n');
|
||||
|
||||
const expectedFunctions = [
|
||||
'moveEventFileV2',
|
||||
'createEquipment',
|
||||
'updateEquipment',
|
||||
'deleteEquipment',
|
||||
'getEquipment',
|
||||
'createContainer',
|
||||
'updateContainer',
|
||||
'deleteContainer',
|
||||
'createEvent',
|
||||
'updateEvent',
|
||||
'deleteEvent',
|
||||
'createMaintenance',
|
||||
'updateMaintenance',
|
||||
'createOption',
|
||||
'updateOption',
|
||||
'deleteOption',
|
||||
'createUser',
|
||||
'updateUser',
|
||||
'updateEquipmentStatus'
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const funcName of expectedFunctions) {
|
||||
if (functions[funcName]) {
|
||||
console.log(`✓ ${funcName}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(`✗ ${funcName} - MANQUANTE`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📊 Résultats: ${passed} passées, ${failed} échouées`);
|
||||
|
||||
if (failed > 0) {
|
||||
console.log('\n❌ Certaines fonctions sont manquantes !');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('\n✅ Toutes les fonctions sont présentes !');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
165
em2rp/functions/utils/auth.js
Normal file
165
em2rp/functions/utils/auth.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Utilitaires d'authentification et d'autorisation
|
||||
*/
|
||||
const admin = require('firebase-admin');
|
||||
const logger = require('firebase-functions/logger');
|
||||
|
||||
/**
|
||||
* Vérifie le token Firebase et retourne l'utilisateur
|
||||
*/
|
||||
async function authenticateUser(req) {
|
||||
if (!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) {
|
||||
throw new Error('Unauthorized: No token provided');
|
||||
}
|
||||
|
||||
const idToken = req.headers.authorization.split('Bearer ')[1];
|
||||
try {
|
||||
const decodedToken = await admin.auth().verifyIdToken(idToken);
|
||||
return decodedToken;
|
||||
} catch (e) {
|
||||
logger.error("Error verifying Firebase ID token:", e);
|
||||
throw new Error('Unauthorized: Invalid token');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les données utilisateur depuis Firestore
|
||||
*/
|
||||
async function getUserData(uid) {
|
||||
const userDoc = await admin.firestore().collection('users').doc(uid).get();
|
||||
if (!userDoc.exists) {
|
||||
return null;
|
||||
}
|
||||
return { uid, ...userDoc.data() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les permissions d'un rôle
|
||||
*/
|
||||
async function getRolePermissions(roleRef) {
|
||||
if (!roleRef) return [];
|
||||
|
||||
let roleId;
|
||||
if (typeof roleRef === 'string') {
|
||||
roleId = roleRef;
|
||||
} else if (roleRef.id) {
|
||||
roleId = roleRef.id;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
const roleDoc = await admin.firestore().collection('roles').doc(roleId).get();
|
||||
if (!roleDoc.exists) return [];
|
||||
|
||||
return roleDoc.data().permissions || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur a une permission spécifique
|
||||
*/
|
||||
async function hasPermission(uid, requiredPermission) {
|
||||
const userData = await getUserData(uid);
|
||||
if (!userData) return false;
|
||||
|
||||
const permissions = await getRolePermissions(userData.role);
|
||||
return permissions.includes(requiredPermission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur est admin
|
||||
*/
|
||||
async function isAdmin(uid) {
|
||||
const userData = await getUserData(uid);
|
||||
if (!userData) return false;
|
||||
|
||||
let roleId;
|
||||
const roleField = userData.role;
|
||||
if (typeof roleField === 'string') {
|
||||
roleId = roleField;
|
||||
} else if (roleField && roleField.id) {
|
||||
roleId = roleField.id;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return roleId === 'ADMIN';
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur est assigné à un événement
|
||||
*/
|
||||
async function isAssignedToEvent(uid, eventId) {
|
||||
const eventDoc = await admin.firestore().collection('events').doc(eventId).get();
|
||||
if (!eventDoc.exists) return false;
|
||||
|
||||
const eventData = eventDoc.data();
|
||||
const workforce = eventData.workforce || [];
|
||||
|
||||
// workforce contient des références DocumentReference
|
||||
return workforce.some(ref => {
|
||||
if (typeof ref === 'string') return ref === uid;
|
||||
if (ref && ref.id) return ref.id === uid;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware d'authentification pour les Cloud Functions HTTP
|
||||
*/
|
||||
async function authMiddleware(req, res, next) {
|
||||
try {
|
||||
const decodedToken = await authenticateUser(req);
|
||||
req.user = decodedToken;
|
||||
req.uid = decodedToken.uid;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware de vérification de permission
|
||||
*/
|
||||
function requirePermission(permission) {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
const hasAccess = await hasPermission(req.uid, permission);
|
||||
if (!hasAccess) {
|
||||
res.status(403).json({ error: `Forbidden: Requires permission '${permission}'` });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(403).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware admin uniquement
|
||||
*/
|
||||
async function requireAdmin(req, res, next) {
|
||||
try {
|
||||
const adminAccess = await isAdmin(req.uid);
|
||||
if (!adminAccess) {
|
||||
res.status(403).json({ error: 'Forbidden: Admin access required' });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(403).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
authenticateUser,
|
||||
getUserData,
|
||||
getRolePermissions,
|
||||
hasPermission,
|
||||
isAdmin,
|
||||
isAssignedToEvent,
|
||||
authMiddleware,
|
||||
requirePermission,
|
||||
requireAdmin,
|
||||
};
|
||||
|
||||
117
em2rp/functions/utils/helpers.js
Normal file
117
em2rp/functions/utils/helpers.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Helpers pour la manipulation de données Firestore
|
||||
*/
|
||||
const admin = require('firebase-admin');
|
||||
|
||||
/**
|
||||
* Convertit les Timestamps Firestore en ISO strings pour JSON
|
||||
*/
|
||||
function serializeTimestamps(data) {
|
||||
if (!data) return data;
|
||||
|
||||
const result = { ...data };
|
||||
|
||||
for (const key in result) {
|
||||
if (result[key] && result[key].toDate && typeof result[key].toDate === 'function') {
|
||||
// C'est un Timestamp Firestore
|
||||
result[key] = result[key].toDate().toISOString();
|
||||
} else if (result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) {
|
||||
// Objet imbriqué
|
||||
result[key] = serializeTimestamps(result[key]);
|
||||
} else if (Array.isArray(result[key])) {
|
||||
// Tableau
|
||||
result[key] = result[key].map(item =>
|
||||
item && typeof item === 'object' ? serializeTimestamps(item) : item
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit les ISO strings en Timestamps Firestore
|
||||
*/
|
||||
function deserializeTimestamps(data, timestampFields = []) {
|
||||
if (!data) return data;
|
||||
|
||||
const result = { ...data };
|
||||
|
||||
for (const field of timestampFields) {
|
||||
if (result[field] && typeof result[field] === 'string') {
|
||||
result[field] = admin.firestore.Timestamp.fromDate(new Date(result[field]));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit les références DocumentReference en IDs
|
||||
*/
|
||||
function serializeReferences(data) {
|
||||
if (!data) return data;
|
||||
|
||||
const result = { ...data };
|
||||
|
||||
for (const key in result) {
|
||||
if (result[key] && result[key].path && typeof result[key].path === 'string') {
|
||||
// C'est une DocumentReference
|
||||
result[key] = result[key].id;
|
||||
} else if (Array.isArray(result[key])) {
|
||||
result[key] = result[key].map(item => {
|
||||
if (item && item.path && typeof item.path === 'string') {
|
||||
return item.id;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Masque les champs sensibles selon les permissions
|
||||
*/
|
||||
function maskSensitiveFields(data, canViewSensitive) {
|
||||
if (canViewSensitive) return data;
|
||||
|
||||
const masked = { ...data };
|
||||
|
||||
// Masquer les prix si pas de permission manage_equipment
|
||||
delete masked.purchasePrice;
|
||||
delete masked.rentalPrice;
|
||||
|
||||
return masked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination helper
|
||||
*/
|
||||
function paginate(query, limit = 50, startAfter = null) {
|
||||
let paginatedQuery = query.limit(limit);
|
||||
|
||||
if (startAfter) {
|
||||
paginatedQuery = paginatedQuery.startAfter(startAfter);
|
||||
}
|
||||
|
||||
return paginatedQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtre les événements annulés
|
||||
*/
|
||||
function filterCancelledEvents(events) {
|
||||
return events.filter(event => event.status !== 'CANCELLED');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
serializeTimestamps,
|
||||
deserializeTimestamps,
|
||||
serializeReferences,
|
||||
maskSensitiveFields,
|
||||
paginate,
|
||||
filterCancelledEvents,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user