diff --git a/em2rp/firestore.rules b/em2rp/firestore.rules deleted file mode 100644 index afb1777..0000000 --- a/em2rp/firestore.rules +++ /dev/null @@ -1,90 +0,0 @@ -rules_version = '2'; - -service cloud.firestore { - match /databases/{database}/documents { - // Fonction pour vérifier si l'utilisateur est authentifié - function isAuthenticated() { - return request.auth != null; - } - - function getUserRole() { - let userData = get(/databases/$(database)/documents/users/$(request.auth.uid)).data; - return userData != null ? userData.role : null; - } - - // Fonction pour vérifier si l'utilisateur est un admin - function isAdmin() { - return isAuthenticated() && getUserRole() == 'ADMIN'; - } - - function isOwner(userId) { - return isAuthenticated() && request.auth.uid == userId; - } - - // Nouvelle fonction pour vérifier si un CREW est assigné à un événement du client - function isAssignedToClientEvent(clientId) { - let events = getAfter(/databases/$(database)/documents/events) - .where("clientId", "==", clientId) - .where("assignedUsers." + request.auth.uid, "==", true).limit(1); - return events.size() > 0; - } - - // Fonction pour vérifier si c'est le premier utilisateur - function isFirstUser() { - return !exists(/databases/$(database)/documents/users); - } - - // Fonction pour vérifier si c'est une mise à jour de l'UID - function isUidUpdate() { - return request.resource.data.diff(resource.data).affectedKeys().hasOnly(['uid']); - } - - // Règles pour la collection users - match /users/{userId} { - allow read: if isAuthenticated() && (isAdmin() || isOwner(userId)); - // Permettre la création si admin OU si l'utilisateur crée son propre document - allow create: if isAdmin() || (isAuthenticated() && request.auth.uid == userId); - allow update: if isAdmin() || - (isOwner(userId) && - request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['phoneNumber', 'profilePhotoUrl', 'firstName', 'lastName', 'role'])); - allow delete: if isAdmin(); - } - - // Règles pour la collection clients - match /clients/{clientId} { - // Lecture : - // - Les admins peuvent tout voir - // - Les CREW ne peuvent voir que les clients liés à leurs événements - allow read: if isAdmin() || - (getUserRole() == 'CREW' && isAssignedToClientEvent(clientId)); - - // Création, modification et suppression : Seuls les admins - allow create, update, delete: if isAdmin(); - } - - // Règles pour la collection events (prestations) - match /events/{eventId} { - allow read: if isAdmin() || - (isAuthenticated() && (resource.data.assignedUsers[request.auth.uid] == true)); - allow create, update: if isAdmin(); - allow delete: if isAdmin(); - } - - // Règles pour la collection quotes (devis) - match /quotes/{quoteId} { - allow read, write: if isAdmin(); - } - - // Règles pour la collection invoices (factures) - match /invoices/{invoiceId} { - allow read, write: if isAdmin(); - } - - // Règles pour les autres collections - match /{document=**} { - // Par défaut, refuser l'accès - allow read, write: if false; - } - } -} \ No newline at end of file diff --git a/em2rp/lib/main.dart b/em2rp/lib/main.dart index 826c480..d6042f9 100644 --- a/em2rp/lib/main.dart +++ b/em2rp/lib/main.dart @@ -96,8 +96,8 @@ class MyApp extends StatelessWidget { '/login': (context) => const LoginPage(), '/calendar': (context) => const AuthGuard(child: CalendarPage()), '/my_account': (context) => const AuthGuard(child: MyAccountPage()), - '/user_management': (context) => - const AuthGuard(requiredRole: "ADMIN", child: UserManagementPage()), + '/user_management': (context) => const AuthGuard( + requiredPermission: "view_all_users", child: UserManagementPage()), '/reset_password': (context) { final args = ModalRoute.of(context)!.settings.arguments as Map; diff --git a/em2rp/lib/models/user_model.dart b/em2rp/lib/models/user_model.dart index 14ee3fc..8105dba 100644 --- a/em2rp/lib/models/user_model.dart +++ b/em2rp/lib/models/user_model.dart @@ -46,7 +46,7 @@ class UserModel { return { 'firstName': firstName, 'lastName': lastName, - 'role': role, + 'role': FirebaseFirestore.instance.collection('roles').doc(role), 'profilePhotoUrl': profilePhotoUrl, 'email': email, 'phoneNumber': phoneNumber, diff --git a/em2rp/lib/providers/users_provider.dart b/em2rp/lib/providers/users_provider.dart index 2d31661..2453d65 100644 --- a/em2rp/lib/providers/users_provider.dart +++ b/em2rp/lib/providers/users_provider.dart @@ -38,14 +38,16 @@ class UsersProvider with ChangeNotifier { } /// Mise à jour d'un utilisateur - Future updateUser(UserModel user) async { + Future updateUser(UserModel user, {String? roleId}) async { try { await _firestore.collection('users').doc(user.uid).update({ 'firstName': user.firstName, 'lastName': user.lastName, 'email': user.email, 'phoneNumber': user.phoneNumber, - 'role': user.role, + 'role': roleId != null + ? _firestore.collection('roles').doc(roleId) + : user.role, 'profilePhotoUrl': user.profilePhotoUrl, }); @@ -77,8 +79,8 @@ class UsersProvider with ChangeNotifier { await _userService.resetPassword(email); } - Future createUserWithEmailInvite( - BuildContext context, UserModel user) async { + Future createUserWithEmailInvite(BuildContext context, UserModel user, + {String? roleId}) async { String? authUid; try { @@ -115,7 +117,9 @@ class UsersProvider with ChangeNotifier { 'lastName': user.lastName, 'email': user.email, 'phoneNumber': user.phoneNumber, - 'role': user.role, + 'role': roleId != null + ? _firestore.collection('roles').doc(roleId) + : user.role, 'profilePhotoUrl': user.profilePhotoUrl, 'createdAt': FieldValue.serverTimestamp(), }); @@ -143,7 +147,7 @@ class UsersProvider with ChangeNotifier { lastName: user.lastName, email: user.email, phoneNumber: user.phoneNumber, - role: user.role, + role: roleId ?? user.role, profilePhotoUrl: user.profilePhotoUrl, ); _users.add(newUser); diff --git a/em2rp/lib/utils/auth_guard_widget.dart b/em2rp/lib/utils/auth_guard_widget.dart index 46c5b90..c84c4df 100644 --- a/em2rp/lib/utils/auth_guard_widget.dart +++ b/em2rp/lib/utils/auth_guard_widget.dart @@ -5,13 +5,12 @@ import 'package:em2rp/views/login_page.dart'; class AuthGuard extends StatelessWidget { final Widget child; - final String? - requiredRole; // Si non null, la page est réservée à ce rôle (ex: "ADMIN") + final String? requiredPermission; const AuthGuard({ super.key, required this.child, - this.requiredRole, + this.requiredPermission, }); @override @@ -20,13 +19,12 @@ class AuthGuard extends StatelessWidget { // Si l'utilisateur n'est pas connecté if (localAuthProvider.currentUser == null) { - // Retourne la page de connexion. - // Vous pouvez aussi déclencher une redirection automatique si nécessaire. return const LoginPage(); } - // Si la page requiert un rôle spécifique et que l'utilisateur ne le possède pas - if (requiredRole != null && localAuthProvider.role != requiredRole) { + // Si la page requiert une permission spécifique et que l'utilisateur ne la possède pas + if (requiredPermission != null && + !localAuthProvider.hasPermission(requiredPermission!)) { return Scaffold( appBar: AppBar(title: const Text("Accès refusé")), body: const Center( diff --git a/em2rp/lib/utils/constants.dart b/em2rp/lib/utils/constants.dart deleted file mode 100644 index ad670dd..0000000 --- a/em2rp/lib/utils/constants.dart +++ /dev/null @@ -1,3 +0,0 @@ -class Constants { - static const List userRoles = ['USER', 'ADMIN']; -} diff --git a/em2rp/lib/views/calendar_page.dart b/em2rp/lib/views/calendar_page.dart index bbbec35..7c6a0fb 100644 --- a/em2rp/lib/views/calendar_page.dart +++ b/em2rp/lib/views/calendar_page.dart @@ -57,7 +57,7 @@ class _CalendarPageState extends State { Widget build(BuildContext context) { final eventProvider = Provider.of(context); final localUserProvider = Provider.of(context); - final isAdmin = localUserProvider.role == 'ADMIN'; + final isAdmin = localUserProvider.hasPermission('view_all_users'); final isMobile = MediaQuery.of(context).size.width < 600; if (eventProvider.isLoading) { diff --git a/em2rp/lib/views/user_management_page.dart b/em2rp/lib/views/user_management_page.dart index 6257991..ab4a954 100644 --- a/em2rp/lib/views/user_management_page.dart +++ b/em2rp/lib/views/user_management_page.dart @@ -9,6 +9,7 @@ import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/permission_gate.dart'; import 'package:em2rp/models/role_model.dart'; import 'package:em2rp/views/widgets/custom_app_bar.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; class UserManagementPage extends StatefulWidget { const UserManagementPage({super.key}); @@ -110,7 +111,20 @@ class _UserManagementPageState extends State { final lastNameController = TextEditingController(); final emailController = TextEditingController(); final phoneController = TextEditingController(); - String selectedRole = 'ADMIN'; + String? selectedRoleId; + List availableRoles = []; + bool isLoadingRoles = true; + + Future _loadRoles() async { + final snapshot = + await FirebaseFirestore.instance.collection('roles').get(); + availableRoles = snapshot.docs + .map((doc) => RoleModel.fromMap(doc.data(), doc.id)) + .toList(); + selectedRoleId = + availableRoles.isNotEmpty ? availableRoles.first.id : null; + isLoadingRoles = false; + } InputDecoration buildInputDecoration(String label, IconData icon) { return InputDecoration( @@ -130,164 +144,162 @@ class _UserManagementPageState extends State { showDialog( context: context, - builder: (context) => Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Container( - width: 400, - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( + builder: (context) => FutureBuilder( + future: _loadRoles(), + builder: (context, snapshot) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Container( + width: 400, + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Icon(Icons.person_add, color: AppColors.rouge), - const SizedBox(width: 12), - Text( - 'Nouvel utilisateur', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: AppColors.noir, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 24), - SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: firstNameController, - decoration: - buildInputDecoration('Prénom', Icons.person_outline), - ), - const SizedBox(height: 16), - TextField( - controller: lastNameController, - decoration: buildInputDecoration('Nom', Icons.person), - ), - const SizedBox(height: 16), - TextField( - controller: emailController, - decoration: - buildInputDecoration('Email', Icons.email_outlined), - keyboardType: TextInputType.emailAddress, - ), - const SizedBox(height: 16), - TextField( - controller: phoneController, - decoration: buildInputDecoration( - 'Téléphone', Icons.phone_outlined), - keyboardType: TextInputType.phone, - ), - const SizedBox(height: 16), - DropdownButtonFormField( - value: selectedRole, - decoration: buildInputDecoration( - 'Rôle', Icons.admin_panel_settings_outlined), - items: ['ADMIN', 'CREW'].map((String role) { - return DropdownMenuItem( - value: role, - child: Text(role), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - selectedRole = newValue; - } - }, - ), - ], - ), - ), - const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), - ), - child: const Text( - 'Annuler', - style: TextStyle(color: AppColors.gris), - ), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: () async { - if (emailController.text.isEmpty || - firstNameController.text.isEmpty || - lastNameController.text.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Veuillez remplir tous les champs obligatoires'), - backgroundColor: Colors.red, - ), - ); - return; - } - - try { - final newUser = UserModel( - uid: '', // Sera généré par Firebase - firstName: firstNameController.text, - lastName: lastNameController.text, - email: emailController.text, - phoneNumber: phoneController.text, - role: selectedRole, - profilePhotoUrl: '', - ); - - final scaffoldMessenger = ScaffoldMessenger.of(context); - await Provider.of(context, listen: false) - .createUserWithEmailInvite(context, newUser); - - if (context.mounted) { - Navigator.pop(context); - scaffoldMessenger.showSnackBar( - const SnackBar( - content: Text('Invitation envoyée avec succès'), - backgroundColor: Colors.green, + Row( + children: [ + const Icon(Icons.person_add, color: AppColors.rouge), + const SizedBox(width: 12), + Text( + 'Nouvel utilisateur', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: AppColors.noir, + fontWeight: FontWeight.bold, ), - ); - } - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Erreur lors de la création: ${e.toString()}'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.rouge, - padding: const EdgeInsets.symmetric( - horizontal: 24, vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), ), + ], + ), + const SizedBox(height: 24), + SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: firstNameController, + decoration: buildInputDecoration( + 'Prénom', Icons.person_outline), + ), + const SizedBox(height: 16), + TextField( + controller: lastNameController, + decoration: buildInputDecoration('Nom', Icons.person), + ), + const SizedBox(height: 16), + TextField( + controller: emailController, + decoration: buildInputDecoration( + 'Email', Icons.email_outlined), + keyboardType: TextInputType.emailAddress, + ), + const SizedBox(height: 16), + TextField( + controller: phoneController, + decoration: buildInputDecoration( + 'Téléphone', Icons.phone_outlined), + keyboardType: TextInputType.phone, + ), + const SizedBox(height: 16), + isLoadingRoles + ? const CircularProgressIndicator() + : DropdownButtonFormField( + value: selectedRoleId, + decoration: buildInputDecoration('Rôle', + Icons.admin_panel_settings_outlined), + items: availableRoles.map((role) { + return DropdownMenuItem( + value: role.id, + child: Text(role.name), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + selectedRoleId = newValue; + } + }, + ), + ], ), - child: const Text( - 'Inviter', - style: TextStyle(color: AppColors.blanc), - ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + ), + child: const Text( + 'Annuler', + style: TextStyle(color: AppColors.gris), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () async { + if (emailController.text.isEmpty || + firstNameController.text.isEmpty || + lastNameController.text.isEmpty || + selectedRoleId == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Veuillez remplir tous les champs obligatoires'), + backgroundColor: Colors.red, + ), + ); + return; + } + try { + final newUser = UserModel( + uid: '', // Sera généré par Firebase + firstName: firstNameController.text, + lastName: lastNameController.text, + email: emailController.text, + phoneNumber: phoneController.text, + role: selectedRoleId!, + profilePhotoUrl: '', + ); + await Provider.of(context, + listen: false) + .createUserWithEmailInvite(context, newUser, + roleId: selectedRoleId); + Navigator.pop(context); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Erreur lors de la création: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.rouge, + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Inviter', + style: TextStyle(color: AppColors.blanc), + ), + ), + ], ), ], ), - ], - ), - ), + ), + ); + }, ), ); } diff --git a/em2rp/lib/views/widgets/calendar_widgets/event_details.dart b/em2rp/lib/views/widgets/calendar_widgets/event_details.dart index f8a88c1..ff42354 100644 --- a/em2rp/lib/views/widgets/calendar_widgets/event_details.dart +++ b/em2rp/lib/views/widgets/calendar_widgets/event_details.dart @@ -31,7 +31,7 @@ class EventDetails extends StatelessWidget { ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); final currentIndex = sortedEvents.indexWhere((e) => e.id == event.id); final localUserProvider = Provider.of(context); - final isAdmin = localUserProvider.role == 'ADMIN'; + final isAdmin = localUserProvider.hasPermission('view_all_users'); return Card( margin: const EdgeInsets.all(16), diff --git a/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart b/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart index da8aea1..c36a00f 100644 --- a/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart +++ b/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart @@ -3,6 +3,8 @@ import 'package:provider/provider.dart'; import 'package:em2rp/models/user_model.dart'; import 'package:em2rp/providers/users_provider.dart'; import 'package:em2rp/utils/colors.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:em2rp/models/role_model.dart'; class EditUserDialog extends StatefulWidget { final UserModel user; @@ -17,9 +19,9 @@ class _EditUserDialogState extends State { late final TextEditingController lastNameController; late final TextEditingController emailController; late final TextEditingController phoneController; - String selectedRole = ''; - - static const List roles = ['ADMIN', 'CREW']; + String? selectedRoleId; + List availableRoles = []; + bool isLoadingRoles = true; @override void initState() { @@ -28,7 +30,20 @@ class _EditUserDialogState extends State { lastNameController = TextEditingController(text: widget.user.lastName); emailController = TextEditingController(text: widget.user.email); phoneController = TextEditingController(text: widget.user.phoneNumber); - selectedRole = widget.user.role.isEmpty ? roles.first : widget.user.role; + _loadRoles(); + } + + Future _loadRoles() async { + final snapshot = await FirebaseFirestore.instance.collection('roles').get(); + setState(() { + availableRoles = snapshot.docs + .map((doc) => RoleModel.fromMap(doc.data(), doc.id)) + .toList(); + selectedRoleId = widget.user.role.isEmpty + ? (availableRoles.isNotEmpty ? availableRoles.first.id : null) + : widget.user.role; + isLoadingRoles = false; + }); } @override @@ -111,24 +126,26 @@ class _EditUserDialogState extends State { keyboardType: TextInputType.phone, ), const SizedBox(height: 16), - DropdownButtonFormField( - value: selectedRole, - decoration: _buildInputDecoration( - 'Rôle', Icons.admin_panel_settings_outlined), - items: roles.map((String role) { - return DropdownMenuItem( - value: role, - child: Text(role), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - setState(() { - selectedRole = newValue; - }); - } - }, - ), + isLoadingRoles + ? const CircularProgressIndicator() + : DropdownButtonFormField( + value: selectedRoleId, + decoration: _buildInputDecoration( + 'Rôle', Icons.admin_panel_settings_outlined), + items: availableRoles.map((role) { + return DropdownMenuItem( + value: role.id, + child: Text(role.name), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + setState(() { + selectedRoleId = newValue; + }); + } + }, + ), ], ), ), @@ -149,16 +166,17 @@ class _EditUserDialogState extends State { ), const SizedBox(width: 8), ElevatedButton( - onPressed: () { + onPressed: () async { + if (selectedRoleId == null) return; final updatedUser = widget.user.copyWith( firstName: firstNameController.text, lastName: lastNameController.text, email: emailController.text, phoneNumber: phoneController.text, - role: selectedRole, + role: selectedRoleId, ); - Provider.of(context, listen: false) - .updateUser(updatedUser); + await Provider.of(context, listen: false) + .updateUser(updatedUser, roleId: selectedRoleId); Navigator.pop(context); }, style: ElevatedButton.styleFrom(