Back

Implémenter la Gestion des Rôles Firebase avec Flutter

 

Implémenter la Gestion des Rôles Firebase avec Flutter

  1. Objectif

    • À l’issue de ce tutoriel, vous serez capable d’intégrer un système complet de gestion des rôles dans une application Flutter en utilisant Firebase Authentication et Cloud Firestore, avec VS Code comme environnement de développement.
    • Vous apprendrez à :
      • Créer et configurer un projet Firebase avec Auth et Firestore.
      • Structurer votre projet Flutter selon l’architecture MVC (Modèle – Vue – Contrôleur).
      • Définir un enum UserRole pour représenter les 4 rôles.
      • Créer une interface de connexion unique redirigeant chaque utilisateur vers son dashboard.
      • Permettre à l’admin de créer tous les comptes avec email, mot de passe et rôle.
      • Sécuriser Firestore avec des Security Rules basées sur les rôles.
      • Gérer les erreurs de connexion et naviguer entre les écrans.
  2. Présentation

    • La gestion des rôles (Role-Based Access Control — RBAC) est un concept fondamental en développement d’applications multi-utilisateurs. Plutôt que de donner les mêmes droits à tous les utilisateurs, on définit des rôles, et chaque rôle dispose de permissions spécifiques.
    • Dans ce tutoriel, nous utilisons deux services Firebase :
      • Firebase Authentication : gère l’identité (email + mot de passe) de chaque utilisateur.
      • Cloud Firestore : stocke le profil de l’utilisateur, notamment son rôle.
    • 💡 Concept clé : Le rôle N’EST PAS stocké dans Firebase Auth. Il est stocké dans Firestore (collection users), et lu après chaque connexion pour décider vers quel écran rediriger l’utilisateur.
  3. Les 4 rôles de l’application

    • L’application distingue 4 types d’utilisateurs, chacun avec ses propres permissions :
      • admin 🔴 : Crée et supprime tous les comptes utilisateurs, voit tous les utilisateurs.
      • enseignant 🟢 : Crée et gère ses propres cours (propriétaire des cours).
      • etudiant 🔵 : Consulte les cours disponibles et peut s’inscrire.
      • responsable_etudiant 🟠 : Supervise la liste des étudiants et tous les cours.
    • ⚠️ Important : Tous les utilisateurs se connectent via la MÊME interface (LoginView). C’est après la connexion que l’application lit le rôle dans Firestore et redirige vers le bon dashboard.
  4. Architecture du projet (MVC)

    • Le projet est organisé selon le pattern MVC (Modèle – Vue – Contrôleur) avec la structure suivante :
      • lib/
        ├── main.dart                          ← Point d'entrée de l'application
        ├── firebase_options.dart              ← Configuration Firebase (généré automatiquement)
        │
        ├── models/                            ← 📦 MODÈLES (données)
        │   ├── user_model.dart                ← Modèle utilisateur + enum UserRole
        │   └── cours_model.dart               ← Modèle cours
        │
        ├── controllers/                       ← 🧠 CONTRÔLEURS (logique métier)
        │   ├── auth_controller.dart           ← Connexion / Déconnexion / Lecture rôle
        │   └── user_controller.dart           ← CRUD utilisateurs et cours
        │
        └── views/                             ← 🖥️  VUES (interface utilisateur)
            ├── login_view.dart                ← Interface de connexion unique
            ├── admin/
            │   └── admin_dashboard.dart       ← 🔴 Dashboard Administrateur
            ├── enseignant/
            │   └── enseignant_dashboard.dart  ← 🟢 Dashboard Enseignant
            ├── etudiant/
            │   └── etudiant_dashboard.dart    ← 🔵 Dashboard Étudiant
            └── responsable/
                └── responsable_dashboard.dart ← 🟠 Dashboard Responsable Étudiant

  5. Configuration du projet

    1. Étape 1 : Créer un projet Firebase
      • Va sur Firebase Console
      • Clique sur “Ajouter un projet”
      • Donne un nom à ton projet, accepte les conditions, clique sur Continuer
      • Active Firebase Authentication → méthode Email/Mot de passe
      • Active Cloud Firestore en mode production
    2. Étape 2 : Ajouter Firebase à ton projet Flutter
      • Créer une nouvelle application Flutter
        • flutter create gestion_roles
        • Ouvrez le projet dans un IDE (Android Studio, VS Code…)
      • Se connecter à Firebase et configurer FlutterFire CLI
        • Si ce n’est pas encore fait, installe FlutterFire CLI :dart pub global activate flutterfire_cli
        • Puis connecte-toi :flutterfire login
        • Et configure Firebase pour ce projet :flutterfire configure
        • Lors de la configuration :
          • Choisis ton projet Firebase existant.
          • Sélectionne Android comme plateforme.
          • Entre le bon nom de package (ex: com.example.gestionroles).
          • Cela génère un fichier firebase_options.dart dans lib/.
    3. Configurer Firebase dans le projet Flutter
      • a. Copier le google-services.json :
        • Téléchargé depuis la console Firebase (section Android).
        • Colle-le dans : android/app/google-services.json
      • b. Modifier android/build.gradle :
        • buildscript {
              dependencies {
                  classpath("com.google.gms:google-services:4.3.15")
              }
              repositories {
                  google()
                  mavenCentral()
              }
          }

      • c. Modifier android/app/build.gradle :
        • plugins {
              id("com.android.application")
              id("kotlin-android")
              id("dev.flutter.flutter-gradle-plugin")
              id("com.google.gms.google-services")
          }

      • d. Modifier le fichier : android/app/build.gradle.kts
      • Dans ton fichier android/app/build.gradle.kts, ajoute (ou modifie) ce bloc dans android :
        • android {
              ndkVersion = "27.0.12077973"
              // ... le reste de ta configuration Android
          }

      • e. Ajouter les dépendances dans pubspec.yaml :
        • dependencies:
            flutter:
              sdk: flutter
            firebase_core:   ^2.24.2
            firebase_auth:   ^4.16.0
            cloud_firestore: ^4.14.0

      • f. Créer le premier compte Admin manuellement :
        • Dans Firebase Console → Authentication, créez un utilisateur email/mot de passe.
        • Notez son UID généré automatiquement.
        • Dans Firestore → collection users, créez un document avec cet UID comme ID :
        • {
            "email":     "admin@univ.tn",
            "nom":       "Admin123",
            "prenom":    "Super",
            "role":      "admin",
            "createdAt": [timestamp actuel]
          }

  6. Créer le compte Administrateur

    • Avant de lancer l’application, vous devez créer manuellement le compte administrateur. Contrairement aux autres utilisateurs qui seront créés depuis l’application, le premier compte admin doit être configuré directement dans la console Firebase en deux étapes obligatoires : une dans Firebase Authentication et une dans Firestore.
    • ⚠️ Pourquoi deux étapes ? Firebase Auth vérifie uniquement l’identité (email + mot de passe). Le rôle de l’utilisateur est stocké dans Firestore. Sans le document Firestore, l’application ne peut pas connaître le rôle et retournera null après la connexion.
      1. Étape 1 : Créer le compte dans Firebase Authentication
        • Allez sur console.firebase.google.com
        • Votre projet → AuthenticationUsersAdd user
        • Saisissez l’email et le mot de passe de l’admin
        • Cliquez Add user
        • ⚠️ Copiez l’UID généré (ex : abc123xyz...) — vous en aurez besoin à l’étape suivante
      2. Étape 2 : Créer le document dans Firestore ⚠️ OBLIGATOIRE
        • Sans cette étape, l’admin peut se connecter mais l’application ne trouvera pas son rôle et retournera null.
        • Votre projet → Firestore → collection usersAdd document
        • Dans Document ID, collez l’UID exact copié à l’étape 1
        • Ajoutez les champs suivants :
          • {
              "email":     "admin@univ.tn",   // type : string
              "nom":       "Admin",           // type : string
              "prenom":    "Super",           // type : string
              "role":      "admin",           // type : string  ← valeur exacte obligatoire
              "createdAt": [date actuelle]    // type : timestamp
            }

        • ⚠️ La valeur du champ role doit être exactement "admin" (en minuscule), c’est la valeur lue par UserRoleExtension.fromString() dans le modèle.
    • Pourquoi ces deux étapes sont-elles nécessaires ?
      • Firebase Auth  →  vérifie email + mot de passe ✅
        Firestore      →  stocke le rôle "admin"        ✅
                                  ↓
           AuthController.signIn() lit le champ "role" dans Firestore
                                  ↓
                 _redirectByRole() → AdminDashboard 🔴

    • Une fois ce compte admin créé, il pourra se connecter via l’application et créer tous les autres comptes (enseignants, étudiants, responsables) directement depuis son dashboard sans passer par la console Firebase.
  7. Fichiers réalisés

    1. Fichier models/user_model.dart
      • Ce fichier définit l’enum UserRole et le modèle UserModel avec les méthodes de conversion vers/depuis Firestore :
        • // File: lib/models/user_model.dart
           
          /// 🎭 ENUM DES RÔLES DISPONIBLES
          enum UserRole {
            admin,
            enseignant,
            etudiant,
            responsable_etudiant,
          }
           
          /// Extension pour convertir le rôle en/depuis String (Firestore)
          extension UserRoleExtension on UserRole {
            String get name {
              switch (this) {
                case UserRole.admin:                return 'admin';
                case UserRole.enseignant:           return 'enseignant';
                case UserRole.etudiant:             return 'etudiant';
                case UserRole.responsable_etudiant: return 'responsable_etudiant';
              }
            }
           
            String get displayName {
              switch (this) {
                case UserRole.admin:                return 'Administrateur';
                case UserRole.enseignant:           return 'Enseignant';
                case UserRole.etudiant:             return 'Étudiant';
                case UserRole.responsable_etudiant: return 'Responsable Étudiant';
              }
            }
           
            static UserRole fromString(String value) {
              switch (value) {
                case 'admin':                return UserRole.admin;
                case 'enseignant':           return UserRole.enseignant;
                case 'etudiant':             return UserRole.etudiant;
                case 'responsable_etudiant': return UserRole.responsable_etudiant;
                default: throw Exception('Rôle inconnu: $value');
              }
            }
          }
           
          /// 👤 MODÈLE UTILISATEUR
          class UserModel {
            final String   uid;
            final String   email;
            final String   nom;
            final String   prenom;
            final UserRole role;
            final DateTime createdAt;
           
            UserModel({
              required this.uid,
              required this.email,
              required this.nom,
              required this.prenom,
              required this.role,
              required this.createdAt,
            });
           
            /// Depuis Firestore → UserModel
            factory UserModel.fromMap(String uid, Map<String, dynamic> map) {
              return UserModel(
                uid:       uid,
                email:     map['email']  ?? '',
                nom:       map['nom']    ?? '',
                prenom:    map['prenom'] ?? '',
                role:      UserRoleExtension.fromString(map['role'] ?? 'etudiant'),
                createdAt: DateTime.now(),
              );
            }
           
            /// UserModel → Firestore
            Map<String, dynamic> toMap() {
              return {
                'email':     email,
                'nom':       nom,
                'prenom':    prenom,
                'role':      role.name,
                'createdAt': createdAt,
              };
            }
           
            String get fullName => '$prenom $nom';
          }

    2. Fichier controllers/auth_controller.dart
      • Le contrôleur d’authentification gère la connexion, la déconnexion et la lecture du rôle depuis Firestore :
        • // File: lib/controllers/auth_controller.dart
          import 'package:firebase_auth/firebase_auth.dart';
          import 'package:cloud_firestore/cloud_firestore.dart';
          import '../models/user_model.dart';
           
          /// 🔐 CONTRÔLEUR D'AUTHENTIFICATION
          class AuthController {
            final FirebaseAuth      _auth      = FirebaseAuth.instance;
            final FirebaseFirestore _firestore = FirebaseFirestore.instance;
           
            /// 🔑 CONNEXION avec email + mot de passe
            Future<UserModel?> signIn(String email, String password) async {
              try {
                final credential = await _auth.signInWithEmailAndPassword(
                  email:    email.trim(),
                  password: password,
                );
                if (credential.user == null) return null;
                // Récupérer le profil + rôle depuis Firestore
                return await getUserById(credential.user!.uid);
              } on FirebaseAuthException catch (e) {
                throw _mapAuthError(e);
              }
            }
           
            /// 👤 Lire le profil utilisateur depuis Firestore
            Future<UserModel?> getUserById(String uid) async {
              final doc = await _firestore.collection('users').doc(uid).get();
              if (!doc.exists) return null;
              return UserModel.fromMap(doc.id, doc.data()!);
            }
           
            /// 🚪 DÉCONNEXION
            Future<void> signOut() async => await _auth.signOut();
           
            /// 🛠️ Convertir les erreurs Firebase en messages lisibles
            String _mapAuthError(FirebaseAuthException e) {
              switch (e.code) {
                case 'invalid-credential': return 'Email ou mot de passe incorrect.';
                case 'user-not-found':     return 'Aucun compte trouvé avec cet email.';
                case 'wrong-password':     return 'Mot de passe incorrect.';
                case 'user-disabled':      return 'Ce compte a été désactivé.';
                case 'too-many-requests':  return 'Trop de tentatives. Réessayez plus tard.';
                default: return 'Erreur: ${e.message}';
              }
            }
          }

    3. Fichier controllers/user_controller.dart
      • Ce contrôleur gère la création des comptes (réservée à l’admin) et toutes les opérations sur les cours :
        • // File: lib/controllers/user_controller.dart
          import 'package:firebase_auth/firebase_auth.dart';
          import 'package:cloud_firestore/cloud_firestore.dart';
          import '../models/user_model.dart';
           
          /// 👥 CONTRÔLEUR UTILISATEURS
          class UserController {
            final FirebaseAuth      _auth = FirebaseAuth.instance;
            final FirebaseFirestore _db   = FirebaseFirestore.instance;
           
            // ── ADMIN : CRÉER UN COMPTE ──────────────────────────────────
            Future<UserModel> createUser({
              required String   email,
              required String   password,
              required String   nom,
              required String   prenom,
              required UserRole role,
            }) async {
              // 1. Créer dans Firebase Auth
              final credential = await _auth.createUserWithEmailAndPassword(
                email: email.trim(), password: password,
              );
              final uid = credential.user!.uid;
           
              // 2. Sauvegarder le profil + rôle dans Firestore
              final user = UserModel(
                uid: uid, email: email.trim(),
                nom: nom.trim(), prenom: prenom.trim(),
                role: role, createdAt: DateTime.now(),
              );
              await _db.collection('users').doc(uid).set(user.toMap());
              return user;
            }
           
            // ── LIRE TOUS LES UTILISATEURS (admin) ──────────────────────
            Future<List<UserModel>> getAllUsers() async {
              final snap = await _db.collection('users')
                  .orderBy('createdAt', descending: true).get();
              return snap.docs
                  .map((d) => UserModel.fromMap(d.id, d.data()))
                  .toList();
            }
           
            // ── LIRE PAR RÔLE (responsable) ─────────────────────────────
            Future<List<UserModel>> getUsersByRole(UserRole role) async {
              final snap = await _db.collection('users')
                  .where('role', isEqualTo: role.name).get();
              return snap.docs
                  .map((d) => UserModel.fromMap(d.id, d.data()))
                  .toList();
            }
           
            // ── SUPPRIMER UN UTILISATEUR (admin) ────────────────────────
            Future<void> deleteUserFromFirestore(String uid) async {
              await _db.collection('users').doc(uid).delete();
            }
           
            // ── CRÉER UN COURS (enseignant) ──────────────────────────────
            Future<void> createCours({
              required String enseignantId,
              required String enseignantNom,
              required String titre,
              required String description,
              required String matiere,
            }) async {
              await _db.collection('cours').add({
                'titre':           titre,
                'description':     description,
                'matiere':         matiere,
                'enseignantId':    enseignantId,
                'enseignantNom':   enseignantNom,
                'nombreEtudiants': 0,
                'createdAt':       FieldValue.serverTimestamp(),
              });
            }
           
            // ── COURS D'UN ENSEIGNANT ────────────────────────────────────
            Future<List<Map<String, dynamic>>> getCoursParEnseignant(String id) async {
              final snap = await _db.collection('cours')
                  .where('enseignantId', isEqualTo: id).get();
              return snap.docs.map((d) => {'id': d.id, ...d.data()}).toList();
            }
           
            // ── TOUS LES COURS ───────────────────────────────────────────
            Future<List<Map<String, dynamic>>> getTousCours() async {
              final snap = await _db.collection('cours')
                  .orderBy('createdAt', descending: true).get();
              return snap.docs.map((d) => {'id': d.id, ...d.data()}).toList();
            }
           
            Future<void> deleteCours(String id) async {
              await _db.collection('cours').doc(id).delete();
            }
          }

    4. Fichier main.dart
      • // File: lib/main.dart
        import 'package:flutter/material.dart';
        import 'package:firebase_core/firebase_core.dart';
        import 'firebase_options.dart';
        import 'views/login_view.dart';
         
        void main() async {
          WidgetsFlutterBinding.ensureInitialized();
          await Firebase.initializeApp(
            options: DefaultFirebaseOptions.currentPlatform,
          );
          runApp(const MyApp());
        }
         
        class MyApp extends StatelessWidget {
          const MyApp({super.key});
         
          @override
          Widget build(BuildContext context) {
            return MaterialApp(
              title: 'Gestion des Rôles Firebase',
              debugShowCheckedModeBanner: false,
              theme: ThemeData(
                colorScheme: ColorScheme.fromSeed(seedColor: Color(0xFF1A237E)),
                useMaterial3: true,
              ),
              // Page d'accueil = Login unique pour tous les rôles
              home: const LoginView(),
              routes: { '/': (context) => const LoginView() },
            );
          }
        }

    5. Fichier views/login_view.dart
      • Interface de connexion unique pour tous les rôles. Après authentification, l’utilisateur est redirigé vers son dashboard selon son rôle :
        • // File: lib/views/login_view.dart
          import 'package:flutter/material.dart';
          import '../controllers/auth_controller.dart';
          import '../models/user_model.dart';
          import 'admin/admin_dashboard.dart';
          import 'enseignant/enseignant_dashboard.dart';
          import 'etudiant/etudiant_dashboard.dart';
          import 'responsable/responsable_dashboard.dart';
           
          class LoginView extends StatefulWidget {
            const LoginView({super.key});
            @override
            State<LoginView> createState() => _LoginViewState();
          }
           
          class _LoginViewState extends State<LoginView> {
            final _formKey            = GlobalKey<FormState>();
            final _emailController    = TextEditingController();
            final _passwordController = TextEditingController();
            final _authController     = AuthController();
            bool    _isLoading   = false;
            bool    _obscurePass = true;
            String? _errorMessage;
           
            Future<void> _login() async {
              if (!_formKey.currentState!.validate()) return;
              setState(() { _isLoading = true; _errorMessage = null; });
              try {
                final user = await _authController.signIn(
                  _emailController.text, _passwordController.text);
                if (user == null) {
                  setState(() => _errorMessage = 'Utilisateur introuvable dans la base.');
                  return;
                }
                if (!mounted) return;
                _redirectByRole(user);
              } catch (e) {
                setState(() => _errorMessage = e.toString());
              } finally {
                if (mounted) setState(() => _isLoading = false);
              }
            }
           
            /// 🔀 Redirection vers le bon dashboard selon le rôle
            void _redirectByRole(UserModel user) {
              Widget destination;
              switch (user.role) {
                case UserRole.admin:
                  destination = AdminDashboard(currentUser: user);       break;
                case UserRole.enseignant:
                  destination = EnseignantDashboard(currentUser: user);  break;
                case UserRole.etudiant:
                  destination = EtudiantDashboard(currentUser: user);    break;
                case UserRole.responsable_etudiant:
                  destination = ResponsableDashboard(currentUser: user); break;
              }
              Navigator.of(context).pushReplacement(
                MaterialPageRoute(builder: (_) => destination),
              );
            }
           
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                body: Container(
                  decoration: const BoxDecoration(
                    gradient: LinearGradient(
                      begin: Alignment.topLeft,
                      end: Alignment.bottomRight,
                      colors: [Color(0xFF1A237E), Color(0xFF283593), Color(0xFF3949AB)],
                    ),
                  ),
                  child: SafeArea(
                    child: Center(
                      child: SingleChildScrollView(
                        padding: const EdgeInsets.all(24),
                        child: Form(
                          key: _formKey,
                          child: Column(
                            children: [
                              const Icon(Icons.school_rounded, size: 80, color: Colors.white),
                              const SizedBox(height: 16),
                              const Text('Gestion des Rôles',
                                style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold,
                                                 color: Colors.white)),
                              const SizedBox(height: 32),
                              TextFormField(
                                controller: _emailController,
                                decoration: const InputDecoration(
                                  labelText: 'Adresse email',
                                  prefixIcon: Icon(Icons.email_outlined),
                                  filled: true, fillColor: Colors.white,
                                ),
                                validator: (v) => v!.isEmpty ? 'Email requis' : null,
                              ),
                              const SizedBox(height: 16),
                              TextFormField(
                                controller: _passwordController,
                                obscureText: _obscurePass,
                                decoration: InputDecoration(
                                  labelText: 'Mot de passe',
                                  prefixIcon: const Icon(Icons.lock_outline),
                                  filled: true, fillColor: Colors.white,
                                  suffixIcon: IconButton(
                                    icon: Icon(_obscurePass
                                      ? Icons.visibility_off_outlined
                                      : Icons.visibility_outlined),
                                    onPressed: () =>
                                      setState(() => _obscurePass = !_obscurePass),
                                  ),
                                ),
                                validator: (v) =>
                                  v!.length < 6 ? 'Minimum 6 caractères' : null,
                              ),
                              if (_errorMessage != null) ...[
                                const SizedBox(height: 12),
                                Text(_errorMessage!,
                                  style: const TextStyle(color: Colors.redAccent)),
                              ],
                              const SizedBox(height: 24),
                              ElevatedButton(
                                onPressed: _isLoading ? null : _login,
                                child: _isLoading
                                  ? const CircularProgressIndicator(color: Colors.white)
                                  : const Text('Se connecter'),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              );
            }
          }

    6. Fichier views/admin/admin_dashboard.dart
      • Le dashboard admin comporte deux onglets : création de comptes et liste des utilisateurs :
        • // File: lib/views/admin/admin_dashboard.dart
          import 'package:flutter/material.dart';
          import '../../controllers/auth_controller.dart';
          import '../../controllers/user_controller.dart';
          import '../../models/user_model.dart';
           
          class AdminDashboard extends StatefulWidget {
            final UserModel currentUser;
            const AdminDashboard({super.key, required this.currentUser});
            @override
            State<AdminDashboard> createState() => _AdminDashboardState();
          }
           
          class _AdminDashboardState extends State<AdminDashboard>
              with SingleTickerProviderStateMixin {
            final _userController = UserController();
            final _authController = AuthController();
            late TabController _tabController;
            List<UserModel> _users = [];
           
            // Champs du formulaire de création
            final _formKey      = GlobalKey<FormState>();
            final _emailCtrl    = TextEditingController();
            final _passwordCtrl = TextEditingController();
            final _nomCtrl      = TextEditingController();
            final _prenomCtrl   = TextEditingController();
            UserRole _selectedRole = UserRole.etudiant;
            bool    _creating   = false;
            String? _createError;
            String? _createSuccess;
           
            @override
            void initState() {
              super.initState();
              _tabController = TabController(length: 2, vsync: this);
              _loadUsers();
            }
           
            Future<void> _loadUsers() async {
              final users = await _userController.getAllUsers();
              setState(() => _users = users);
            }
           
            /// 🆕 L'admin crée un compte avec email, mot de passe et rôle
            Future<void> _createUser() async {
              if (!_formKey.currentState!.validate()) return;
              setState(() { _creating = true; _createError = null; });
              try {
                await _userController.createUser(
                  email:    _emailCtrl.text,
                  password: _passwordCtrl.text,
                  nom:      _nomCtrl.text,
                  prenom:   _prenomCtrl.text,
                  role:     _selectedRole,
                );
                setState(() =>
                  _createSuccess = '✅ Compte créé : ${_prenomCtrl.text} (${_selectedRole.name})');
                _loadUsers();
              } catch (e) {
                setState(() => _createError = e.toString());
              } finally {
                setState(() => _creating = false);
              }
            }
           
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  backgroundColor: Colors.red.shade700,
                  title: Text('Admin — ${widget.currentUser.fullName}',
                    style: const TextStyle(color: Colors.white)),
                  actions: [
                    IconButton(
                      icon: const Icon(Icons.logout, color: Colors.white),
                      onPressed: () async {
                        await _authController.signOut();
                        Navigator.of(context).pushReplacementNamed('/');
                      },
                    )
                  ],
                  bottom: TabBar(
                    controller: _tabController,
                    indicatorColor: Colors.white,
                    labelColor: Colors.white,
                    tabs: const [
                      Tab(icon: Icon(Icons.person_add), text: 'Créer un compte'),
                      Tab(icon: Icon(Icons.people),     text: 'Utilisateurs'),
                    ],
                  ),
                ),
                body: TabBarView(
                  controller: _tabController,
                  children: [
                    _buildCreateUserTab(),
                    _buildUsersListTab(),
                  ],
                ),
              );
            }
           
            Widget _buildCreateUserTab() {
              return SingleChildScrollView(
                padding: const EdgeInsets.all(20),
                child: Form(
                  key: _formKey,
                  child: Column(
                    children: [
                      TextFormField(controller: _prenomCtrl,
                        decoration: const InputDecoration(labelText: 'Prénom'),
                        validator: (v) => v!.isEmpty ? 'Requis' : null),
                      TextFormField(controller: _nomCtrl,
                        decoration: const InputDecoration(labelText: 'Nom'),
                        validator: (v) => v!.isEmpty ? 'Requis' : null),
                      TextFormField(controller: _emailCtrl,
                        decoration: const InputDecoration(labelText: 'Email'),
                        validator: (v) => v!.isEmpty ? 'Requis' : null),
                      TextFormField(controller: _passwordCtrl,
                        obscureText: true,
                        decoration: const InputDecoration(labelText: 'Mot de passe'),
                        validator: (v) => v!.length < 6 ? 'Min. 6 caractères' : null),
                      const SizedBox(height: 16),
                      // Sélection du rôle via des chips colorées
                      Wrap(
                        spacing: 8,
                        children: UserRole.values.map((role) =>
                          ChoiceChip(
                            label: Text(role.name),
                            selected: _selectedRole == role,
                            onSelected: (_) => setState(() => _selectedRole = role),
                          ),
                        ).toList(),
                      ),
                      if (_createError   != null) Text(_createError!,
                        style: const TextStyle(color: Colors.red)),
                      if (_createSuccess != null) Text(_createSuccess!,
                        style: const TextStyle(color: Colors.green)),
                      const SizedBox(height: 16),
                      ElevatedButton.icon(
                        icon: const Icon(Icons.person_add),
                        label: Text(_creating ? 'Création...' : 'Créer le compte'),
                        onPressed: _creating ? null : _createUser,
                      ),
                    ],
                  ),
                ),
              );
            }
           
            Widget _buildUsersListTab() {
              return ListView.builder(
                padding: const EdgeInsets.all(16),
                itemCount: _users.length,
                itemBuilder: (_, i) {
                  final u = _users[i];
                  return ListTile(
                    leading: const Icon(Icons.account_circle),
                    title:    Text(u.fullName),
                    subtitle: Text('${u.email} — ${u.role.name}'),
                    trailing: IconButton(
                      icon: const Icon(Icons.delete_outline, color: Colors.red),
                      onPressed: () async {
                        await _userController.deleteUserFromFirestore(u.uid);
                        _loadUsers();
                      },
                    ),
                  );
                },
              );
            }
          }

    7. Fichier views/enseignant/enseignant_dashboard.dart
      • L’enseignant est propriétaire de ses cours. Ses cours sont filtrés par son UID dans Firestore :
        • // File: lib/views/enseignant/enseignant_dashboard.dart
          import 'package:flutter/material.dart';
          import '../../controllers/auth_controller.dart';
          import '../../controllers/user_controller.dart';
          import '../../models/user_model.dart';
           
          class EnseignantDashboard extends StatefulWidget {
            final UserModel currentUser;
            const EnseignantDashboard({super.key, required this.currentUser});
            @override
            State<EnseignantDashboard> createState() => _EnseignantDashboardState();
          }
           
          class _EnseignantDashboardState extends State<EnseignantDashboard>
              with SingleTickerProviderStateMixin {
            final _userController = UserController();
            final _authController = AuthController();
            late TabController _tabController;
            List<Map<String, dynamic>> _cours = [];
           
            final _formKey   = GlobalKey<FormState>();
            final _titreCtrl = TextEditingController();
            final _descCtrl  = TextEditingController();
            final _matCtrl   = TextEditingController();
           
            @override
            void initState() {
              super.initState();
              _tabController = TabController(length: 2, vsync: this);
              _loadCours();
            }
           
            /// Chargement des cours de cet enseignant uniquement
            Future<void> _loadCours() async {
              final cours = await _userController
                  .getCoursParEnseignant(widget.currentUser.uid);
              setState(() => _cours = cours);
            }
           
            /// Création d'un cours (enseignant = propriétaire)
            Future<void> _createCours() async {
              if (!_formKey.currentState!.validate()) return;
              await _userController.createCours(
                titre:        _titreCtrl.text,
                description:  _descCtrl.text,
                matiere:      _matCtrl.text,
                enseignantId:  widget.currentUser.uid,
                enseignantNom: widget.currentUser.fullName,
              );
              _loadCours();
              _tabController.animateTo(1);
            }
           
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  backgroundColor: Colors.green.shade700,
                  title: Text('Enseignant — ${widget.currentUser.fullName}',
                    style: const TextStyle(color: Colors.white)),
                  actions: [
                    IconButton(
                      icon: const Icon(Icons.logout, color: Colors.white),
                      onPressed: () async {
                        await _authController.signOut();
                        Navigator.of(context).pushReplacementNamed('/');
                      },
                    )
                  ],
                  bottom: TabBar(
                    controller: _tabController,
                    indicatorColor: Colors.white,
                    labelColor: Colors.white,
                    tabs: const [
                      Tab(icon: Icon(Icons.add_circle_outline), text: 'Nouveau cours'),
                      Tab(icon: Icon(Icons.menu_book),          text: 'Mes cours'),
                    ],
                  ),
                ),
                body: TabBarView(
                  controller: _tabController,
                  children: [
                    SingleChildScrollView(
                      padding: const EdgeInsets.all(20),
                      child: Form(
                        key: _formKey,
                        child: Column(children: [
                          TextFormField(controller: _titreCtrl,
                            decoration: const InputDecoration(labelText: 'Titre du cours'),
                            validator: (v) => v!.isEmpty ? 'Requis' : null),
                          TextFormField(controller: _matCtrl,
                            decoration: const InputDecoration(labelText: 'Matière'),
                            validator: (v) => v!.isEmpty ? 'Requis' : null),
                          TextFormField(controller: _descCtrl, maxLines: 3,
                            decoration: const InputDecoration(labelText: 'Description'),
                            validator: (v) => v!.isEmpty ? 'Requis' : null),
                          const SizedBox(height: 16),
                          ElevatedButton.icon(
                            icon: const Icon(Icons.save),
                            label: const Text('Publier le cours'),
                            onPressed: _createCours,
                          ),
                        ]),
                      ),
                    ),
                    ListView.builder(
                      padding: const EdgeInsets.all(16),
                      itemCount: _cours.length,
                      itemBuilder: (_, i) {
                        final c = _cours[i];
                        return ListTile(
                          leading: const Icon(Icons.menu_book, color: Colors.green),
                          title:    Text(c['titre'] ?? ''),
                          subtitle: Text(c['matiere'] ?? ''),
                          trailing: IconButton(
                            icon: const Icon(Icons.delete_outline, color: Colors.red),
                            onPressed: () async {
                              await _userController.deleteCours(c['id']);
                              _loadCours();
                            },
                          ),
                        );
                      },
                    ),
                  ],
                ),
              );
            }
          }

    8. Fichier views/etudiant/etudiant_dashboard.dart
      • L’étudiant a un accès en lecture seule sur les cours. Il peut rechercher et s’inscrire :
        • // File: lib/views/etudiant/etudiant_dashboard.dart
          import 'package:flutter/material.dart';
          import '../../controllers/auth_controller.dart';
          import '../../controllers/user_controller.dart';
          import '../../models/user_model.dart';
           
          class EtudiantDashboard extends StatefulWidget {
            final UserModel currentUser;
            const EtudiantDashboard({super.key, required this.currentUser});
            @override
            State<EtudiantDashboard> createState() => _EtudiantDashboardState();
          }
           
          class _EtudiantDashboardState extends State<EtudiantDashboard> {
            final _userController = UserController();
            final _authController = AuthController();
            List<Map<String, dynamic>> _cours = [];
            String _searchQuery = '';
           
            @override
            void initState() {
              super.initState();
              _loadCours();
            }
           
            Future<void> _loadCours() async {
              final cours = await _userController.getTousCours();
              setState(() => _cours = cours);
            }
           
            /// Filtrage des cours par recherche (titre, matière, enseignant)
            List<Map<String, dynamic>> get _filteredCours {
              if (_searchQuery.isEmpty) return _cours;
              return _cours.where((c) {
                final q = _searchQuery.toLowerCase();
                return (c['titre']         ?? '').toLowerCase().contains(q)
                    || (c['matiere']       ?? '').toLowerCase().contains(q)
                    || (c['enseignantNom'] ?? '').toLowerCase().contains(q);
              }).toList();
            }
           
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  backgroundColor: Colors.blue.shade700,
                  title: Text('Étudiant — ${widget.currentUser.fullName}',
                    style: const TextStyle(color: Colors.white)),
                  actions: [
                    IconButton(
                      icon: const Icon(Icons.logout, color: Colors.white),
                      onPressed: () async {
                        await _authController.signOut();
                        Navigator.of(context).pushReplacementNamed('/');
                      },
                    )
                  ],
                ),
                body: Column(children: [
                  Padding(
                    padding: const EdgeInsets.all(12),
                    child: TextField(
                      onChanged: (v) => setState(() => _searchQuery = v),
                      decoration: const InputDecoration(
                        hintText: 'Rechercher un cours...',
                        prefixIcon: Icon(Icons.search),
                        border: OutlineInputBorder(),
                      ),
                    ),
                  ),
                  Expanded(
                    child: ListView.builder(
                      padding: const EdgeInsets.all(16),
                      itemCount: _filteredCours.length,
                      itemBuilder: (_, i) {
                        final c = _filteredCours[i];
                        return Card(
                          child: ListTile(
                            leading: const Icon(Icons.menu_book, color: Colors.blue),
                            title:    Text(c['titre']   ?? ''),
                            subtitle: Text('${c['matiere'] ?? ''} — Prof. ${c['enseignantNom'] ?? ''}'),
                            trailing: OutlinedButton(
                              onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
                                SnackBar(content: Text('Inscrit à "${c['titre']}"!')),
                              ),
                              child: const Text("S'inscrire"),
                            ),
                          ),
                        );
                      },
                    ),
                  ),
                ]),
              );
            }
          }

    9. Fichier views/responsable/responsable_dashboard.dart
      • Le responsable étudiant a une vue de supervision en lecture seule sur les étudiants et les cours :
        • // File: lib/views/responsable/responsable_dashboard.dart
          import 'package:flutter/material.dart';
          import '../../controllers/auth_controller.dart';
          import '../../controllers/user_controller.dart';
          import '../../models/user_model.dart';
           
          class ResponsableDashboard extends StatefulWidget {
            final UserModel currentUser;
            const ResponsableDashboard({super.key, required this.currentUser});
            @override
            State<ResponsableDashboard> createState() => _ResponsableDashboardState();
          }
           
          class _ResponsableDashboardState extends State<ResponsableDashboard>
              with SingleTickerProviderStateMixin {
            final _userController = UserController();
            final _authController = AuthController();
            late TabController _tabController;
            List<UserModel>              _etudiants = [];
            List<Map<String, dynamic>>  _cours     = [];
           
            @override
            void initState() {
              super.initState();
              _tabController = TabController(length: 2, vsync: this);
              _loadEtudiants();
              _loadCours();
            }
           
            Future<void> _loadEtudiants() async {
              final users = await _userController.getUsersByRole(UserRole.etudiant);
              setState(() => _etudiants = users);
            }
           
            Future<void> _loadCours() async {
              final cours = await _userController.getTousCours();
              setState(() => _cours = cours);
            }
           
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  backgroundColor: Colors.orange.shade700,
                  title: Text('Responsable — ${widget.currentUser.fullName}',
                    style: const TextStyle(color: Colors.white)),
                  actions: [
                    IconButton(
                      icon: const Icon(Icons.logout, color: Colors.white),
                      onPressed: () async {
                        await _authController.signOut();
                        Navigator.of(context).pushReplacementNamed('/');
                      },
                    )
                  ],
                  bottom: TabBar(
                    controller: _tabController,
                    indicatorColor: Colors.white,
                    labelColor: Colors.white,
                    tabs: const [
                      Tab(icon: Icon(Icons.people),    text: 'Étudiants'),
                      Tab(icon: Icon(Icons.menu_book), text: 'Tous les cours'),
                    ],
                  ),
                ),
                body: TabBarView(
                  controller: _tabController,
                  children: [
                    // Onglet 1 : liste des étudiants
                    ListView.builder(
                      padding: const EdgeInsets.all(16),
                      itemCount: _etudiants.length,
                      itemBuilder: (_, i) {
                        final e = _etudiants[i];
                        return ListTile(
                          leading: CircleAvatar(child: Text(e.prenom[0])),
                          title:    Text(e.fullName),
                          subtitle: Text(e.email),
                        );
                      },
                    ),
                    // Onglet 2 : tous les cours (lecture seule)
                    ListView.builder(
                      padding: const EdgeInsets.all(16),
                      itemCount: _cours.length,
                      itemBuilder: (_, i) {
                        final c = _cours[i];
                        return ListTile(
                          leading: const Icon(Icons.menu_book, color: Colors.orange),
                          title:    Text(c['titre']   ?? ''),
                          subtitle: Text('${c['matiere'] ?? ''} — Prof. ${c['enseignantNom'] ?? ''}'),
                        );
                      },
                    ),
                  ],
                ),
              );
            }
          }

    10. Fichier firestore.rules — Security Rules
      • Copiez ces règles dans Firebase Console → Firestore → Règles pour sécuriser l’accès côté serveur :
        • rules_version = '2';
          service cloud.firestore {
            match /databases/{database}/documents {
           
              // Helper : récupérer le rôle de l'utilisateur connecté
              function getUserRole() {
                return get(/databases/$(database)/documents/
                       users/$(request.auth.uid)).data.role;
              }
              function isAdmin()      { return request.auth != null && getUserRole() == 'admin'; }
              function isEnseignant() { return request.auth != null && getUserRole() == 'enseignant'; }
              function isAuth()       { return request.auth != null; }
           
              // ── Collection users ──────────────────────────────────────
              match /users/{userId} {
                allow read:   if isAuth() && (isAdmin() || request.auth.uid == userId);
                allow write:  if isAdmin();
                allow create: if isAdmin();
                allow delete: if isAdmin();
              }
           
              // ── Collection cours ──────────────────────────────────────
              match /cours/{coursId} {
                allow read:   if isAuth();
                allow create: if isEnseignant()
                              && request.resource.data.enseignantId == request.auth.uid;
                allow update, delete:
                              if isAdmin()
                              || (isEnseignant()
                                 && resource.data.enseignantId == request.auth.uid);
              }
            }
          }




Riadh HAJJI