feat: ajout de la configuration des émulateurs Firebase et mise à jour des services pour utiliser le backend sécurisé

This commit is contained in:
ElPoyo
2026-01-06 23:43:36 +01:00
parent fb6a271f66
commit 13a890606d
24 changed files with 1905 additions and 375 deletions

View 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,
};

View 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,
};