Cette mise à jour majeure refactorise entièrement la gestion des utilisateurs pour la faire passer par des Cloud Functions sécurisées et migre une part importante de la logique métier (gestion des événements, maintenances, containers) du client vers le backend.
**Gestion des Utilisateurs (Backend & Frontend):**
- **Nouvelle fonction `createUserWithInvite` :**
- Crée l'utilisateur dans Firebase Auth avec un mot de passe temporaire.
- Crée le document utilisateur correspondant dans Firestore.
- Envoie automatiquement un e-mail de réinitialisation de mot de passe (via l'API REST de Firebase et `axios`) pour que l'utilisateur définisse son propre mot de passe, améliorant la sécurité et l'expérience d'intégration.
- **Refactorisation de `updateUser` et `deleteUser` :**
- Les anciennes fonctions `onCall` sont remplacées par des fonctions `onRequest` (HTTP) standards, alignées avec le reste de l'API.
- La logique de suppression gère désormais la suppression dans Auth et Firestore.
- **Réinitialisation de Mot de Passe (UI) :**
- Ajout d'un bouton "Réinitialiser le mot de passe" sur la carte utilisateur, permettant aux administrateurs d'envoyer un e-mail de réinitialisation à n'importe quel utilisateur.
- **Amélioration de l'UI :**
- Boîte de dialogue de confirmation améliorée pour la suppression d'un utilisateur.
- Notifications (Snackbars) pour les opérations de création, suppression et réinitialisation de mot de passe.
**Migration de la Logique Métier vers les Cloud Functions:**
- **Gestion de la Préparation d'Événements :**
- Migration complète de la logique de validation des étapes (préparation, chargement, déchargement, retour) du client vers de nouvelles Cloud Functions (`validateEquipmentPreparation`, `validateAllLoading`, etc.).
- Le backend gère désormais la mise à jour des statuts de l'événement (`inProgress`, `completed`) et des équipements (`inUse`, `available`).
- Le code frontend (`EventPreparationService`) a été simplifié pour appeler ces nouvelles fonctions au lieu d'effectuer des écritures directes sur Firestore.
- **Création de Maintenance :**
- La fonction `createMaintenance` gère maintenant la mise à jour des équipements associés (`maintenanceIds`) et la création d'alertes (`maintenanceDue`) si une maintenance est prévue prochainement. La logique client a été supprimée.
- **Suppression de Container :**
- La fonction `deleteContainer` a été améliorée pour nettoyer automatiquement les références (`parentBoxIds`) dans tous les équipements contenus avant de supprimer le container.
**Refactorisation et Corrections (Backend & Frontend) :**
- **Fiabilisation des Appels API (Frontend) :**
- Le `ApiService` a été renforcé pour convertir de manière plus robuste les données (notamment les `Map` de type `_JsonMap`) en JSON standard avant de les envoyer aux Cloud Functions, évitant ainsi des erreurs de sérialisation.
- **Correction des Références (Backend) :**
- La fonction `updateUser` convertit correctement les `roleId` (string) en `DocumentReference` Firestore.
- Sécurisation de la vérification de l'assignation d'un utilisateur à un événement (`workforce`) pour éviter les erreurs sur des références nulles.
- **Dépendance (Backend) :**
- Ajout de la librairie `axios` pour effectuer des appels à l'API REST de Firebase.
304 lines
9.3 KiB
Dart
304 lines
9.3 KiB
Dart
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'dart:convert';
|
|
import 'package:em2rp/config/api_config.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
|
|
/// Interface abstraite pour les opérations API
|
|
/// Permet de changer facilement de backend (Firebase Functions, REST API personnalisé, etc.)
|
|
abstract class ApiService {
|
|
Future<Map<String, dynamic>> call(String functionName, Map<String, dynamic> data);
|
|
Future<T?> get<T>(String endpoint, {Map<String, dynamic>? params});
|
|
Future<T> post<T>(String endpoint, Map<String, dynamic> data);
|
|
Future<T> put<T>(String endpoint, Map<String, dynamic> data);
|
|
Future<void> delete(String endpoint, {Map<String, dynamic>? data});
|
|
}
|
|
|
|
/// Implémentation pour Firebase Cloud Functions
|
|
class FirebaseFunctionsApiService implements ApiService {
|
|
// URL de base - gérée par ApiConfig
|
|
String get _baseUrl => ApiConfig.baseUrl;
|
|
|
|
/// Récupère le token d'authentification Firebase
|
|
Future<String?> _getAuthToken() async {
|
|
final user = FirebaseAuth.instance.currentUser;
|
|
if (user == null) return null;
|
|
return await user.getIdToken();
|
|
}
|
|
|
|
/// Headers par défaut avec authentification
|
|
Future<Map<String, String>> _getHeaders() async {
|
|
final token = await _getAuthToken();
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
if (token != null) 'Authorization': 'Bearer $token',
|
|
};
|
|
}
|
|
|
|
/// Convertit récursivement TOUT en types JSON standards (String, num, bool, List, Map)
|
|
/// Garantit que toutes les Maps sont des Map<String, dynamic> littérales
|
|
dynamic _toJsonSafe(dynamic value) {
|
|
if (value == null) return null;
|
|
|
|
// Types primitifs JSON-safe
|
|
if (value is String || value is num || value is bool) {
|
|
return value;
|
|
}
|
|
|
|
// Types Firestore
|
|
if (value is Timestamp) {
|
|
return value.toDate().toIso8601String();
|
|
}
|
|
if (value is DateTime) {
|
|
return value.toIso8601String();
|
|
}
|
|
if (value is DocumentReference) {
|
|
return value.path;
|
|
}
|
|
if (value is GeoPoint) {
|
|
// Créer une Map littérale explicite
|
|
return <String, dynamic>{
|
|
'latitude': value.latitude,
|
|
'longitude': value.longitude,
|
|
};
|
|
}
|
|
|
|
// Listes - créer une nouvelle List littérale
|
|
if (value is List) {
|
|
final result = <dynamic>[];
|
|
for (final item in value) {
|
|
result.add(_toJsonSafe(item));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Maps - créer une nouvelle Map littérale explicite
|
|
if (value is Map) {
|
|
final result = <String, dynamic>{};
|
|
value.forEach((k, v) {
|
|
final key = k.toString();
|
|
final convertedValue = _toJsonSafe(v);
|
|
result[key] = convertedValue;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
// Type non supporté - retourner en String
|
|
return value.toString();
|
|
}
|
|
|
|
/// Prépare les données pour jsonEncode en faisant un double passage
|
|
Map<String, dynamic> _prepareForJson(Map<String, dynamic> data) {
|
|
try {
|
|
// Premier passage : convertir tous les types Firestore
|
|
final safeData = _toJsonSafe(data);
|
|
|
|
// Deuxième passage : encoder puis décoder pour forcer la normalisation
|
|
// Cela garantit que tout est 100% compatible JSON et élimine tous les _JsonMap
|
|
final jsonString = jsonEncode(safeData);
|
|
final decoded = jsonDecode(jsonString);
|
|
|
|
// Force le type Map<String, dynamic>
|
|
if (decoded is Map) {
|
|
return Map<String, dynamic>.from(decoded);
|
|
}
|
|
|
|
// Fallback - ne devrait jamais arriver
|
|
return Map<String, dynamic>.from(safeData as Map);
|
|
} catch (e) {
|
|
// Si l'encodage échoue, essayer de créer une copie profonde manuelle
|
|
print('[API] Error in _prepareForJson: $e');
|
|
print('[API] Trying manual deep copy...');
|
|
return _deepCopyMap(data);
|
|
}
|
|
}
|
|
|
|
/// Copie profonde manuelle d'une Map pour éviter les _JsonMap
|
|
Map<String, dynamic> _deepCopyMap(Map<String, dynamic> source) {
|
|
final result = <String, dynamic>{};
|
|
source.forEach((key, value) {
|
|
if (value is Map) {
|
|
result[key] = _deepCopyMap(Map<String, dynamic>.from(value));
|
|
} else if (value is List) {
|
|
result[key] = _deepCopyList(value);
|
|
} else {
|
|
result[key] = value;
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
/// Copie profonde manuelle d'une List
|
|
List<dynamic> _deepCopyList(List<dynamic> source) {
|
|
return source.map((item) {
|
|
if (item is Map) {
|
|
return _deepCopyMap(Map<String, dynamic>.from(item));
|
|
} else if (item is List) {
|
|
return _deepCopyList(item);
|
|
} else {
|
|
return item;
|
|
}
|
|
}).toList();
|
|
}
|
|
|
|
@override
|
|
Future<Map<String, dynamic>> call(String functionName, Map<String, dynamic> data) async {
|
|
final url = Uri.parse('$_baseUrl/$functionName');
|
|
final headers = await _getHeaders();
|
|
|
|
// Préparer les données avec double passage pour éviter les _JsonMap
|
|
final preparedData = _prepareForJson(data);
|
|
|
|
// Log pour débogage
|
|
print('[API] Calling $functionName with eventId: ${preparedData['eventId']}');
|
|
|
|
try {
|
|
// Encoder directement avec jsonEncode standard
|
|
final bodyJson = jsonEncode({'data': preparedData});
|
|
|
|
final response = await http.post(
|
|
url,
|
|
headers: headers,
|
|
body: bodyJson,
|
|
);
|
|
|
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
final responseData = jsonDecode(response.body);
|
|
return responseData is Map<String, dynamic> ? responseData : {};
|
|
} else {
|
|
final error = jsonDecode(response.body);
|
|
throw ApiException(
|
|
message: error['error'] ?? 'Unknown error',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
print('[API] Error during request: $e');
|
|
print('[API] Error type: ${e.runtimeType}');
|
|
throw ApiException(
|
|
message: 'Error calling $functionName: $e',
|
|
statusCode: 0,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<T?> get<T>(String endpoint, {Map<String, dynamic>? params}) async {
|
|
final url = Uri.parse('$_baseUrl/$endpoint').replace(queryParameters: params);
|
|
final headers = await _getHeaders();
|
|
|
|
final response = await http.get(url, headers: headers);
|
|
|
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
final responseData = jsonDecode(response.body);
|
|
return responseData as T?;
|
|
} else if (response.statusCode == 404) {
|
|
return null;
|
|
} else {
|
|
final error = jsonDecode(response.body);
|
|
throw ApiException(
|
|
message: error['error'] ?? 'Unknown error',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<T> post<T>(String endpoint, Map<String, dynamic> data) async {
|
|
final url = Uri.parse('$_baseUrl/$endpoint');
|
|
final headers = await _getHeaders();
|
|
|
|
// Préparer les données avec double passage
|
|
final preparedData = _prepareForJson(data);
|
|
|
|
final response = await http.post(
|
|
url,
|
|
headers: headers,
|
|
body: jsonEncode({'data': preparedData}),
|
|
);
|
|
|
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
final responseData = jsonDecode(response.body);
|
|
return responseData as T;
|
|
} else {
|
|
final error = jsonDecode(response.body);
|
|
throw ApiException(
|
|
message: error['error'] ?? 'Unknown error',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<T> put<T>(String endpoint, Map<String, dynamic> data) async {
|
|
final url = Uri.parse('$_baseUrl/$endpoint');
|
|
final headers = await _getHeaders();
|
|
|
|
// Préparer les données avec double passage
|
|
final preparedData = _prepareForJson(data);
|
|
|
|
final response = await http.put(
|
|
url,
|
|
headers: headers,
|
|
body: jsonEncode({'data': preparedData}),
|
|
);
|
|
|
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
final responseData = jsonDecode(response.body);
|
|
return responseData as T;
|
|
} else {
|
|
final error = jsonDecode(response.body);
|
|
throw ApiException(
|
|
message: error['error'] ?? 'Unknown error',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> delete(String endpoint, {Map<String, dynamic>? data}) async {
|
|
final url = Uri.parse('$_baseUrl/$endpoint');
|
|
final headers = await _getHeaders();
|
|
|
|
// Préparer les données avec double passage si data existe
|
|
final preparedData = data != null ? _prepareForJson(data) : null;
|
|
|
|
final response = await http.delete(
|
|
url,
|
|
headers: headers,
|
|
body: preparedData != null ? jsonEncode({'data': preparedData}) : null,
|
|
);
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
final error = jsonDecode(response.body);
|
|
throw ApiException(
|
|
message: error['error'] ?? 'Unknown error',
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Exception personnalisée pour les erreurs API
|
|
class ApiException implements Exception {
|
|
final String message;
|
|
final int statusCode;
|
|
|
|
ApiException({
|
|
required this.message,
|
|
required this.statusCode,
|
|
});
|
|
|
|
@override
|
|
String toString() => 'ApiException($statusCode): $message';
|
|
|
|
bool get isForbidden => statusCode == 403;
|
|
bool get isUnauthorized => statusCode == 401;
|
|
bool get isNotFound => statusCode == 404;
|
|
bool get isConflict => statusCode == 409;
|
|
}
|
|
|
|
/// Instance singleton du service API
|
|
final ApiService apiService = FirebaseFunctionsApiService();
|
|
|