Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Flutter

Travailler avec Firebase Authentication dans Flutter

Un cours complet pour maîtriser Travailler avec Firebase Authentication dans Flutter.

0 chapitres 0 QCM Accès gratuit

Travailler avec Firebase Authentication dans Flutter

  1. Objectif

    • Comprendre les différents modes d'authentification Firebase
    • Implémenter l'authentification email/mot de passe
    • Gérer les états d'authentification dans Flutter
    • Sécuriser l'accès aux données Firestore

    Ajouter Firebase à votre application Flutter

  2. Introduction à Firebase Authentication

    • Qu'est-ce que Firebase Authentication?
    • Firebase Authentication est un service backend puissant et sécurisé fourni par Google qui permet d'authentifier les utilisateurs dans vos applications de manière simple et rapide.
    • Firebase Authentication est un service backend performant proposé par Google Firebase , conçu pour accélérer l'authentification des utilisateurs dans les applications. Prenant en charge diverses méthodes d'authentification, telles que l'adresse e-mail/mot de passe, le numéro de téléphone et les connexions via les réseaux sociaux , Firebase Authentication garantit une authentification et une gestion des identités sécurisées.
    • En quoi consiste exactement ce service ?
    • Imaginez devoir créer vous-même tout un système de :
      • Création de comptes utilisateurs
      • Vérification d'emails
      • Réinitialisation de mots de passe
      • Gestion des sessions
      • Sécurité des données...
    • Firebase Authentication fait tout ce travail complexe pour vous !
  3. Code source de la vidéo
      models/user_model.dart
      /// Modèle représentant un utilisateur dans l'application.
      /// Ce modèle sert à extraire uniquement les informations utiles
      /// de l'objet User retourné par Firebase Authentication.
      class UserModel {
        /// Identifiant unique de l'utilisateur (UID) fourni par Firebase.
        final String uid;
      
        /// Email de l'utilisateur.
        /// Peut être vide si Firebase ne fournit pas d'email (ex: connexion anonyme).
        final String email;
      
        /// Constructeur de la classe UserModel.
        /// Les deux champs sont requis.
        UserModel({required this.uid, required this.email});
      
        /// Factory permettant de créer un UserModel à partir d'un objet
        /// Firebase User (FirebaseAuth.instance.currentUser).
        /// Cela facilite la conversion des données Firebase vers ton modèle local.
        factory UserModel.fromFirebaseUser(user) {
          return UserModel(
            uid: user.uid,           // Récupère l'UID de l'utilisateur Firebase
            email: user.email ?? "", // Récupère l'email ou une chaîne vide si null
          );
        }
      }
      controllers/auth_controller.dart
      import 'package:firebase_auth/firebase_auth.dart';
      
      /// Contrôleur responsable de la gestion de l'authentification
      /// via Firebase Authentication.
      /// 
      /// Ce controller regroupe toutes les opérations :
      /* connexion
      - inscription
      - déconnexion
      - gestion des erreurs*/
      class AuthController {
        /// Instance principale de FirebaseAuth utilisée pour toutes
        /// les opérations d'authentification.
        final FirebaseAuth _auth = FirebaseAuth.instance;
      
        // ------------------------------------------------------------------
        //   Connexion Email / Mot de passe
        // ------------------------------------------------------------------
        /// Tente de connecter un utilisateur via email et mot de passe.
        ///
        /// Retourne :
        ///   • null  → si la connexion réussit
        ///   • String → si une erreur doit être affichée dans l'UI
        Future<String?> login({
          required String email,
          required String password,
        }) async {
          try {
            // Nettoie les champs (supprime espaces au début/fin)
            await _auth.signInWithEmailAndPassword(
              email: email.trim(),
              password: password.trim(),
            );
      
            return null; // aucune erreur → succès
          } on FirebaseAuthException catch (e) {
            // Conversion du code d'erreur Firebase en message lisible
            return getErrorMessage(e);
          }
        }
      
        // ------------------------------------------------------------------
        //   Inscription Email / Mot de passe
        // ------------------------------------------------------------------
        /// Crée un nouveau compte utilisateur avec email et mot de passe.
        ///
        /// Retourne :
        ///   • null  → si l'inscription réussit
        ///   • String → si une erreur doit être affichée
        Future<String?> register({
          required String email,
          required String password,
        }) async {
          try {
            await _auth.createUserWithEmailAndPassword(
              email: email.trim(),
              password: password.trim(),
            );
      
            return null; // succès
          } on FirebaseAuthException catch (e) {
            return getErrorMessage(e); // message d'erreur lisible
          }
        }
      
        // ------------------------------------------------------------------
        //   Déconnexion
        // ------------------------------------------------------------------
        /// Déconnecte l'utilisateur actuellement connecté.
        Future<void> logout() async {
          await _auth.signOut();
        }
      
        // ------------------------------------------------------------------
        //   Vérifier l'utilisateur connecté
        // ------------------------------------------------------------------
        /// Renvoie l'utilisateur actuellement connecté, ou null
        /// s'il n'y a personne.
        User? get currentUser => _auth.currentUser;
      
        /// Émet un flux indiquant en temps réel si un utilisateur
        /// se connecte / se déconnecte.
        ///
        /// Très utile pour gérer la navigation automatique.
        Stream<User?> authStateChanges() => _auth.authStateChanges();
      
        // ------------------------------------------------------------------
        //   Gestion des erreurs Firebase
        // ------------------------------------------------------------------
        /// Convertit un code d'erreur Firebase en message plus lisible
        /// pour l'utilisateur final.
        ///
        /// Exemple : "user-not-found" → "Aucun utilisateur trouvé avec cet email."
        String getErrorMessage(FirebaseAuthException e) {
          switch (e.code) {
            case "user-not-found":
              return "Aucun utilisateur trouvé avec cet email.";
            case "wrong-password":
              return "Mot de passe incorrect.";
            case "invalid-email":
              return "L'email n'est pas valide.";
            case "email-already-in-use":
              return "Cet email est déjà utilisé.";
            case "weak-password":
              return "Le mot de passe est trop faible.";
            case "user-disabled":
              return "Ce compte est désactivé.";
            default:
              return "Erreur : ${e.message}";
          }
        }
      }
      views/login_view.dart
      import 'package:authentification_demo/views/signup_view.dart';
      import 'package:flutter/material.dart';
      import 'package:firebase_auth/firebase_auth.dart';
      
      class LoginView extends StatefulWidget {
        const LoginView({super.key});
      
        @override
        State<LoginView> createState() => _LoginViewState();
      }
      
      class _LoginViewState extends State<LoginView> {
        final emailController = TextEditingController();
        final passwordController = TextEditingController();
        bool isLoading = false;
        String? errorMessage;
      
        Future<void> login() async {
          setState(() {
            isLoading = true;
            errorMessage = null;
          });
      
          try {
            await FirebaseAuth.instance.signInWithEmailAndPassword(
              email: emailController.text.trim(),
              password: passwordController.text.trim(),
            );
          } on FirebaseAuthException catch (e) {
            setState(() {
              errorMessage = e.message;
            });
          } finally {
            setState(() {
              isLoading = false;
            });
          }
        }
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            body: Container(
              width: double.infinity,
              decoration: const BoxDecoration(
                gradient: LinearGradient(
                  colors: [Color(0xFF6A11CB), Color(0xFF2575FC)],
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                ),
              ),
              child: Center(
                child: SingleChildScrollView(
                  child: Container(
                    padding: const EdgeInsets.all(24),
                    margin: const EdgeInsets.symmetric(horizontal: 25),
                    decoration: BoxDecoration(
                      color: Colors.white.withOpacity(0.95),
                      borderRadius: BorderRadius.circular(20),
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black.withOpacity(0.2),
                          blurRadius: 12,
                          offset: const Offset(0, 6),
                        ),
                      ],
                    ),
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        const Icon(
                          Icons.lock_outline,
                          size: 70,
                          color: Color(0xFF6A11CB),
                        ),
                        const SizedBox(height: 15),
                        const Text(
                          "Bienvenue",
                          style: TextStyle(
                            fontSize: 26,
                            fontWeight: FontWeight.bold,
                            color: Color(0xFF6A11CB),
                          ),
                        ),
                        const SizedBox(height: 25),
      
                        // -------- Email ----------
                        TextField(
                          controller: emailController,
                          decoration: InputDecoration(
                            labelText: "Email",
                            prefixIcon: const Icon(Icons.email_outlined),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(15),
                            ),
                          ),
                        ),
      
                        const SizedBox(height: 15),
      
                        // -------- Password ----------
                        TextField(
                          controller: passwordController,
                          obscureText: true,
                          decoration: InputDecoration(
                            labelText: "Mot de passe",
                            prefixIcon: const Icon(Icons.lock_outline),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(15),
                            ),
                          ),
                        ),
      
                        const SizedBox(height: 15),
      
                        // -------- Error Message ----------
                        if (errorMessage != null)
                          Container(
                            padding: const EdgeInsets.all(12),
                            decoration: BoxDecoration(
                              color: Colors.redAccent.withOpacity(0.2),
                              borderRadius: BorderRadius.circular(10),
                            ),
                            child: Text(
                              errorMessage!,
                              style: const TextStyle(
                                color: Colors.red,
                                fontWeight: FontWeight.bold,
                              ),
                              textAlign: TextAlign.center,
                            ),
                          ),
      
                        const SizedBox(height: 20),
      
                        // -------- Login Button ----------
                        SizedBox(
                          width: double.infinity,
                          child: ElevatedButton(
                            onPressed: isLoading ? null : login,
                            style: ElevatedButton.styleFrom(
                              padding: const EdgeInsets.symmetric(vertical: 14),
                              backgroundColor: const Color(0xFF6A11CB),
                              shape: RoundedRectangleBorder(
                                borderRadius: BorderRadius.circular(12),
                              ),
                            ),
                            child: isLoading
                                ? const CircularProgressIndicator(color: Colors.white)
                                : const Text(
                                    "Se connecter",
                                    style: TextStyle(
                                      fontSize: 18,
                                      color: Colors.white,
                                    ),
                                  ),
                          ),
                        ),
      
                        const SizedBox(height: 10),
      
                        TextButton(
                          onPressed: () {
          Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => const SignUpView()),
          );
        },
                          child: const Text(
                            "Créer un compte",
                            style: TextStyle(color: Colors.black54),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          );
        }
      }
      views/signup_view.dart
      import 'package:flutter/material.dart';
      import '../controllers/auth_controller.dart';
      
      class SignUpView extends StatefulWidget {
        const SignUpView({super.key});
      
        @override
        State<SignUpView> createState() => _SignUpViewState();
      }
      
      class _SignUpViewState extends State<SignUpView> {
        final auth = AuthController();
        final emailController = TextEditingController();
        final passwordController = TextEditingController();
        String? errorMessage;
        bool isLoading = false;
      
        Future<void> register() async {
          setState(() { isLoading = true; });
      
          final error = await auth.register(
            email: emailController.text,
            password: passwordController.text,
          );
      
          if (error != null) {
            setState(() => errorMessage = error);
          }
      
          setState(() => isLoading = false);
        }
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            body: Container(
              padding: const EdgeInsets.symmetric(horizontal: 25),
              decoration: const BoxDecoration(
                gradient: LinearGradient(
                  colors: [Color(0xFF11998E), Color(0xFF38EF7D)],
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                ),
              ),
              child: Center(
                child: SingleChildScrollView(
                  child: Container(
                    padding: const EdgeInsets.all(25),
                    decoration: BoxDecoration(
                      color: Colors.white.withOpacity(0.95),
                      borderRadius: BorderRadius.circular(20),
                    ),
                    child: Column(
                      children: [
                        const Text(
                          "Créer un compte",
                          style: TextStyle(
                            fontSize: 26,
                            fontWeight: FontWeight.bold,
                            color: Color(0xFF11998E),
                          ),
                        ),
                        const SizedBox(height: 25),
      
                        TextField(
                          controller: emailController,
                          decoration: InputDecoration(
                            labelText: "Email",
                            prefixIcon: const Icon(Icons.email_outlined),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(15),
                            ),
                          ),
                        ),
      
                        const SizedBox(height: 15),
      
                        TextField(
                          controller: passwordController,
                          obscureText: true,
                          decoration: InputDecoration(
                            labelText: "Mot de passe",
                            prefixIcon: const Icon(Icons.lock_outline),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(15),
                            ),
                          ),
                        ),
      
                        const SizedBox(height: 20),
      
                        if (errorMessage != null)
                          Text(
                            errorMessage!,
                            style: const TextStyle(color: Colors.red),
                            textAlign: TextAlign.center,
                          ),
      
                        const SizedBox(height: 20),
      
                        SizedBox(
                          width: double.infinity,
                          child: ElevatedButton(
                            onPressed: isLoading ? null : register,
                            style: ElevatedButton.styleFrom(
                              padding: const EdgeInsets.symmetric(vertical: 12),
                              backgroundColor: const Color(0xFF11998E),
                              shape: RoundedRectangleBorder(
                                borderRadius: BorderRadius.circular(12),
                              ),
                            ),
                            child: isLoading
                                ? const CircularProgressIndicator(color: Colors.white)
                                : const Text(
                                    "S'inscrire",
                                    style: TextStyle(fontSize: 18),
                                  ),
                          ),
                        ),
      
                        const SizedBox(height: 10),
      
                        TextButton(
                          onPressed: () => Navigator.pop(context),
                          child: const Text("Déjà un compte ? Se connecter"),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
          );
        }
      }
      views/home_view.dart
      import 'package:flutter/material.dart';
      import 'package:firebase_auth/firebase_auth.dart';
      
      class HomeView extends StatelessWidget {
        const HomeView({super.key});
      
        @override
        Widget build(BuildContext context) {
          final user = FirebaseAuth.instance.currentUser;
      
          return Scaffold(
            appBar: AppBar(
              title: const Text("Accueil"),
              backgroundColor: Colors.deepPurple,
              actions: [
                IconButton(
                  icon: const Icon(Icons.logout),
                  onPressed: () async {
                    await FirebaseAuth.instance.signOut();
                    // Redirection vers la page de login
                    Navigator.of(context).pushReplacementNamed('/login');
                  },
                ),
              ],
            ),
            body: Center(
              child: Text(
                "Bienvenue ${user?.email ?? 'Utilisateur'}",
                style: const TextStyle(fontSize: 22),
              ),
            ),
          );
        }
      }
      main.dart
      import 'package:flutter/material.dart';
      import 'package:firebase_core/firebase_core.dart';
      import 'package:firebase_auth/firebase_auth.dart';
      import 'views/login_view.dart';
      import 'views/home_view.dart';
      
      void main() async {
        WidgetsFlutterBinding.ensureInitialized();
        await Firebase.initializeApp();
        runApp(const MyApp());
      }
      
      class MyApp extends StatelessWidget {
        const MyApp({super.key});
      
        @override
        Widget build(BuildContext context) {
          return MaterialApp(
            debugShowCheckedModeBanner: false,
            home: StreamBuilder(
              stream: FirebaseAuth.instance.authStateChanges(),
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  return const HomeView();
                }
                return const LoginView();
              },
            ),
          );
        }
      }
      firebase_options.dart
      // File: lib/firebase_options.dart
      import 'package:firebase_core/firebase_core.dart';
      /*
      Remplacer les valeurs par celles de ton google-services.json :
      current_key → apiKey
      mobilesdk_app_id → appId
      project_id → projectId
      storage_bucket → storageBucket
      project_number → messagingSenderId*/
      
      class DefaultFirebaseOptions {
        static const FirebaseOptions android = FirebaseOptions(
          apiKey: '...................',
          appId: '......................',
          messagingSenderId: '.............',
          projectId: '.................',
          storageBucket: '...................',
        );
      
        static FirebaseOptions get currentPlatform {
          return android;
        }
      }
  4. Fonctionnalités principales de l'authentification Firebase

    • Plusieurs fournisseurs d'authentification : Firebase Authentication prend en charge plusieurs méthodes d'authentification, notamment l'adresse e-mail/mot de passe, le numéro de téléphone, la connexion via les réseaux sociaux (Google, Facebook, Twitter, GitHub), et bien plus encore.
    • Intégration facile: Firebase Authentication fournit des SDK pour les plateformes populaires, ce qui facilite l’intégration de l’authentification dans vos applications en quelques lignes de code seulement.
    • Sécurité: Firebase Authentication gère l'authentification des utilisateurs de manière sécurisée, en utilisant des protocoles de chiffrement et d'authentification conformes aux normes de l'industrie afin de protéger les données et les informations d'identification des utilisateurs.
    • Gestion des utilisateurs: Firebase Authentication fournit des outils pour la gestion des comptes utilisateurs, notamment la création d’utilisateurs , la réinitialisation des mots de passe , la vérification des adresses e-mail et la suppression des comptes .
    • Personnalisation: Vous pouvez personnaliser l'expérience d'authentification dans votre application, notamment en personnalisant l' interface utilisateur , en mettant en œuvre l'authentification multifacteurs et en appliquant des politiques de mots de passe.
  5. Méthodes principales d’authentification

    • Firebase propose 3 méthodes principales d’authentification :
      1. Authentification Email / Mot de passe
        • Nécessite un email + un mot de passe
        • Création d'un compte Firebase classique.
        • L’utilisateur doit saisir ses identifiants.
        • Exemple : _auth.createUserWithEmailAndPassword(email: "...", password: "...");
        • Avantage : Simple et classique
        • Inconvénient: L’utilisateur doit taper un email et un mot de passe.
      2. Authentification via Google Sign-In
        • Ne nécessite ni mot de passe, ni saisie d’email
        • Google fournit automatiquement :l'email,le nom,la photo,un token sécurisé.
        • L’utilisateur clique → Choisit son compte Google → Connecté.
        • Exemple : GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
        • Avantage: Ultra rapide, Pas de mot de passe à mémoriser, Très sécurisé
        • Inconvénient: Nécessite une configuration Firebase + SHA-1/SHA-256
        • Nécessite un compte Google valide
      3. Authentification Anonyme
        • Ne nécessite ni email, ni mot de passe, ni Google
        • Firebase crée un utilisateur temporaire sans identité.
        • Exemple : FirebaseAuth.instance.signInAnonymously();
        • Avantages: Accès immédiat (invité), Parfait pour tester une app, Utile pour les apps où on veut essayer avant de créer un compte
        • Inconvénients : L’utilisateur n’a pas de profil, pas de photo, pas d’email, Si l’utilisateur désinstalle l’app → Compte perdu (sauf s'il est lié plus tard à Google/email)
      4. Résumé visuel
          MéthodeEmail ? Mot de passe ?Pop-up Google ? Identité garantie ?
          Email/Password ✔ oui ✔ oui ✖ non ✔ oui
          Google Sign-In ✔ récupéré automatiquement ✖ non ✔ oui ✔ oui
          Anonyme ✖ non ✖ non ✖ non non
  6. Implémenter l'authentification Firebase

    • Nous utilisons le lien https://console.firebase.google.com/ pour nous connecter à Firebase et cliquons sur « Accéder à la console », sélectionnons le projet que nous avons déjà configuré, puis choisissons le produit d'authentification :
    • Un message concernant le produit apparaît :
    • L'étape suivante consiste à sélectionner les méthodes de connexion que vous allez accepter :
    • Pour ce tutoriel, nous allons utiliser trois méthodes d’authentification : « Adresse e-mail / Mot de passe », « Anonyme » et « Compte Google ». Commencez par cliquer sur « Adresse e-mail / Mot de passe » dans la section « Fournisseurs natifs » afin de l’activer.
    • Cliquez sur le bouton pour activer l'option « Courriel/Mot de passe » (veuillez ne pas utiliser le « Lien courriel » pour le moment, car cela nécessite des étapes de configuration supplémentaires). Cliquez sur « Enregistrer ».
    • Pour le moment, seul le fournisseur « Courriel/Mot de passe » est activé. Cliquez sur « Ajouter un nouveau fournisseur » pour l’étape suivante et choisissez « Anonyme ».
    • Cliquez sur le bouton pour activer le mode « Anonyme », puis sur « Enregistrer » pour passer à l’étape suivante. Dans l’aperçu, cliquez sur « Google ».
    • Pour les autres fournisseurs, activez Google comme fournisseur d'authentification. Une note concerne l'« Empreinte SHA-1 » ; nous y reviendrons. Vous pouvez modifier le « Nom public » ; je conserve la valeur par défaut. Il faut ensuite fournir une « Adresse e-mail d'assistance » ; utilisez celle par défaut de votre compte Google Firebase. Cliquez enfin sur « Enregistrer ».
  7. Connexion Flutter au service Firebase Authentication

    • Pour permettre à votre application Flutter d'utiliser Firebase Authentication, vous devez d'abord ajouter Firebase à votre projet Flutter. Cela implique l'installation des bibliothèques Firebase, la configuration de Firebase dans Flutter, et l'implémentation du code d'authentification.
      1. Ajouter Firebase à Flutter
        • Dans votre projet Flutter, ouvrez le fichier pubspec.yaml et ajoutez les dépendances suivantes :
        dependencies:
        flutter:
        sdk: flutter
        firebase_core: ^3.0.0
        firebase_auth: ^5.0.0
        google_sign_in: ^7.0.0
      2. Exécutez la commande suivante pour installer les dépendances :
      3. flutter pub get
      4. Puis, initialisez Firebase dans votre application. Dans le fichier main.dart, modifiez la fonction main() comme suit :
      5. import 'package:flutter/material.dart';
        import 'package:firebase_core/firebase_core.dart';
        
        void main() async {
        WidgetsFlutterBinding.ensureInitialized();
        await Firebase.initializeApp();
        runApp(const MyApp());
        }
  8. Créer un formulaire d'authentification (Email / Mot de passe)
    • Nous allons créer un formulaire simple dans Flutter avec deux champs de texte : un pour l'email et un pour le mot de passe, ainsi qu'un bouton de connexion.
    • Ajouter un contrôleur d'authentification
        auth_controller.dart
        import 'package:firebase_auth/firebase_auth.dart';
        import 'package:flutter/material.dart';
        
        class AuthController with ChangeNotifier {
          final FirebaseAuth _auth = FirebaseAuth.instance;
        
          User? get currentUser => _auth.currentUser;
        
          // Connexion avec email et mot de passe
          Future<String?> login(String email, String password) async {
            try {
              await _auth.signInWithEmailAndPassword(email: email, password: password);
              return null; // Succès
            } on FirebaseAuthException catch (e) {
              return e.message; // Retourne l'erreur
            }
          }
        
          // Déconnexion
          Future<void> logout() async {
            await _auth.signOut();
          }
        
          // Écouteur d'état
          Stream<User?> authStateChanges() {
            return _auth.authStateChanges();
          }
        }
        
    • Créez login_view.dart dans le dossier views :
        login_view.dart
        import 'package:flutter/material.dart';
        import 'package:provider/provider.dart';
        import '../controllers/auth_controller.dart';
        
        class LoginView extends StatelessWidget {
          final TextEditingController emailCtrl = TextEditingController();
          final TextEditingController passCtrl = TextEditingController();
        
          @override
          Widget build(BuildContext context) {
            final authController = Provider.of<AuthController>(context);
        
            return Scaffold(
              appBar: AppBar(title: const Text("Connexion")),
              body: Padding(
                padding: const EdgeInsets.all(20),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    TextField(
                      controller: emailCtrl,
                      decoration: InputDecoration(labelText: 'Email'),
                    ),
                    const SizedBox(height: 15),
                    TextField(
                      controller: passCtrl,
                      decoration: InputDecoration(labelText: 'Mot de passe'),
                      obscureText: true,
                    ),
                    const SizedBox(height: 20),
                    ElevatedButton(
                      onPressed: () async {
                        final res = await authController.login(
                          emailCtrl.text.trim(),
                          passCtrl.text.trim(),
                        );
                        if (res == null) {
                          // Succès – redirection automatique dans le StreamBuilder (voir étape 3)
                        } else {
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(content: Text(res)),
                          );
                        }
                      },
                      child: const Text('Se connecter'),
                    )
                  ],
                ),
              ),
            );
          }
        }
        
    • Ce code permet à l'utilisateur de se connecter en utilisant son email et son mot de passe enregistrés sur Firebase.
    • Modifier main.dart pour afficher la bonne interface selon la connexion
        main.dart
        import 'package:flutter/material.dart';
        import 'package:firebase_core/firebase_core.dart';
        import 'package:provider/provider.dart';
        import 'firebase_options.dart';
        import 'controllers/livre_controller.dart';
        import 'controllers/auth_controller.dart';
        import 'views/liste_livres_view_recherche.dart';
        import 'views/login_view.dart';
        
        void main() async {
          WidgetsFlutterBinding.ensureInitialized();
        
          await Firebase.initializeApp(
            options: DefaultFirebaseOptions.currentPlatform,
          );
        
          runApp(MyApp());
        }
        
        class MyApp extends StatelessWidget {
          @override
          Widget build(BuildContext context) {
            return MultiProvider(
              providers: [
                Provider<LivreController>(
                  create: (_) => LivreController(),
                ),
                ChangeNotifierProvider<AuthController>(
                  create: (_) => AuthController(),
                ),
              ],
              child: MaterialApp(
                title: 'Ma Bibliothèque',
                theme: ThemeData(
                  useMaterial3: false,
                  primaryColor: const Color(0xFF4CAF50),
                ),
                debugShowCheckedModeBanner: false,
                home: AuthWrapper(),  // Changement important
              ),
            );
          }
        }
        
        class AuthWrapper extends StatelessWidget {
          @override
          Widget build(BuildContext context) {
            final authController = Provider.of<AuthController>(context, listen: false);
        
            return StreamBuilder(
              stream: authController.authStateChanges(),
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return const Scaffold(
                      body: Center(child: CircularProgressIndicator()));
                }
                if (snapshot.hasData) {
                  return const ListeLivresView();  // Authentifié
                }
                return LoginView(); // Non connecté
              },
            );
          }
        }
        
  9. Se connecter avec email et mot de passe dans ton application Flutter
    • Pour te connecter avec email et mot de passe dans ton application Flutter, tu dois d'abord avoir un compte utilisateur créé dans Firebase Authentication (base de données d'utilisateurs Firebase). Voici deux façons de procéder :
      1. Créer un utilisateur directement dans la console Firebase (méthode rapide pour tester)

        • Va dans la console Firebase :https://console.firebase.google.com
        • Sélectionne ton projet.
        • Dans le menu de gauche, clique sur Authentication.
        • Va dans l'onglet Utilisateurs.
        • Clique sur le bouton "Ajouter un utilisateur" en haut à droite.
        • Renseigne un email (ex: test@example.com) et un mot de passe (ex: 12345678).
        • Clique sur Ajouter un utilisateur.
        • Tu peux maintenant utiliser ces identifiants dans ton application Flutter via le formulaire de connexion.
      2. Créer un utilisateur depuis ton application Flutter

        • Tu peux aussi permettre à l'utilisateur de s'inscrire depuis ton app en ajoutant un bouton ou une page d'inscription (SignUp) avec :
        • 
          await FirebaseAuth.instance.createUserWithEmailAndPassword(
            email: emailInput,
            password: passwordInput,
          );
  10. Activation de l’authentification Google dans Flutter

    1. Préparer le projet Flutter
      • Avant de commencer, il faut ajouter les packages nécessaires.
      • Ajouter les dépendances dans pubspec.yaml
          dependencies:
            firebase_core: ^4.2.1
            firebase_auth: ^6.1.2
            google_sign_in: ^6.1.5
      • Puis exécuter :flutter pub get
    2. Configuration côté Firebase
      • Activer Google Sign-in dans Firebase
        • Aller à : Firebase Console → Authentication → Sign-in methods
        • Activer Google
        • Cliquer sur Enable
        • Enregistrer.
        • Activation de l’authentification Google dans Flutter
    3. Ajouter les clés SHA-1 & SHA-256 (ÉTAPE OBLIGATOIRE)
      • Sans cette étape, tu obtiens l’erreur : PlatformException(sign_in_failed, ApiException:10)
      • Pourquoi ? Parce que Google Sign-In mobile nécessite que Firebase reconnaisse ton application via ses empreintes SHA.
        1. Obtenir SHA-1 et SHA-256

          • Méthode 1 - Avec Flutter (la meilleure)
          • Dans le terminal, va dans le dossier android du projet : cd android
          • Puis exécute : ./gradlew signingReport
          • Tu obtiendras un tableau contenant :
            • SHA1: F1:23:A4:...
            • SHA256: 3A:1F:90:...
          • Méthode 2 - Avec Android Studio
            • Ouvrir Android Studio
            • Ouvrir le module Android : android/ → clic droit → Open in Android Studio
            • Aller dans : Gradle → Tasks → android → signingReport
            • Lire les empreintes SHA-1 et SHA-256
          • Méthode 3 - Avec Gradle (Flutter / Android) - Recommandée
          • C’est la méthode la plus fiable. Elle génère les clés pour toutes les variantes (debug / release).
            • Ouvre un terminal dans le dossier racine du projet Flutter
            • Exécute :
              • Windows:
                cd android
                
                .\gradlew signingReport
              • macOS / Linux:
                cd android
                ./gradlew signingReport
            • Copie les valeurs SHA1 et SHA-256 affichées sous la variante debug (et release si tu comptes publier)
        2. Ajouter les empreintes dans Firebase

          • Ouvre la Firebase Console → ton projet.
          • Clique sur Paramètres du projet (icône roue dentée) → Paramètres du projet.
          • Sous Tes apps, sélectionne ton application Android (ou ajoute-la si nécessaire).
          • Dans Empreintes SHA (SHA certificates / SHA fingerprints), clique Ajouter une empreinte.
          • Colle SHA-1, clique enregistrer.
          • Répète pour SHA-256.
          • Enregistrer
          • Télécharger le fichier google-services.json mis à jour
          • Remplacer celui dans : android/app/google-services.json
    4. Ajouter Google Sign-In dans le Controller
      1. Import nécessaire

          import 'package:google_sign_in/google_sign_in.dart';
      2. Ajouter l’instance Google

          final GoogleSignIn _googleSignIn = GoogleSignIn(
            scopes: ['email', 'profile'],
            signInOption: SignInOption.standard,
          );
      3. Méthode signInWithGoogle

          // ------------------------------------------------------------------
          //   MÉTHODE POUR GOOGLE : Connexion via Google Sign-In + Firebase
          // ------------------------------------------------------------------
          Future<String?> signInWithGoogle() async {
            try {
              // 1️- Ouvre la fenêtre de connexion Google
              //    Si l'utilisateur annule (ferme la pop-up), googleUser sera null.
              final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
              if (googleUser == null) return "Connexion annulée";
          
              // 2️- Récupère les informations d’authentification Google :
              //    - accessToken
              //    - idToken
              //    Ces tokens serviront à authentifier l’utilisateur auprès de Firebase.
              final GoogleSignInAuthentication googleAuth =
                  await googleUser.authentication;
          
              // 3️- Crée les credentials Firebase à partir des tokens Google
              //    Firebase utilise ces identifiants pour vérifier la connexion.
              final AuthCredential credential = GoogleAuthProvider.credential(
                accessToken: googleAuth.accessToken, // jeton d’accès
                idToken: googleAuth.idToken,         // jeton d'identité
              );
          
              // 4️- Connexion à Firebase avec les identifiants Google
              //    Si tout est correct, l’utilisateur sera connecté immédiatement.
              await _auth.signInWithCredential(credential);
          
              return null; // Connexion réussie
          
            } on FirebaseAuthException catch (e) {
              // 5️- Gestion des erreurs Firebase
              //    Utilise votre fonction existante pour générer un message propre.
              return getErrorMessage(e);
          
            } catch (e) {
              // 6️- Gestion des autres erreurs (Google Sign-In, réseau, etc.)
              return "Erreur Google Sign-In : $e";
            }
          }
      4. Méthode logout() (déconnexion)

          // ------------------------------------------------------------------
            //   Déconnexion
            // ------------------------------------------------------------------
            /// Déconnecte l'utilisateur actuellement connecté.
            Future<void> logout() async {
              await _googleSignIn.signOut(); // ← AJOUTER CETTE LIGNE POUR GOOGLE
              await _auth.signOut();
            }
    5. Ajouter le bouton Google dans la page de Login
        ElevatedButton.icon(
          onPressed: isLoadingGoogle ? null : loginWithGoogle,
          icon: Icon(Icons.login, color: Colors.red),
          label: isLoadingGoogle
              ? CircularProgressIndicator(strokeWidth: 2)
              : Text("Continuer avec Google"),
        )
    6. Tester - Problèmes courants - Google Sign-In
        Tableau récapitulatif des problèmes, causes et solutions
        Problème Cause Solution
        ApiException:10 ✖ SHA-1 / SHA-256 manquantes Ajouter les empreintes (SHA-1 & SHA-256) dans la console Firebase
        Popup Google en anglais Google Sign-In force la langue selon l’appareil Impossible de forcer directement - laisser la langue système de l’appareil
        Crash ou écran blanc google-services.json non mis à jour Télécharger la nouvelle version de google-services.json depuis Firebase et remplacer celle du projet
        App non reconnue Mauvais package name Vérifier et corriger le package dans AndroidManifest.xml et dans la console Firebase
  • Gestion des rôles utilisateurs avec Firebase Authentication et Flutter

    • Apprendre à combiner l’authentification Firebase et Firestore pour gérer différents rôles (admin, auteur, lecteur) dans une application Flutter.
      1. Introduction
        • Firebase Authentication permet de gérer l’inscription et la connexion des utilisateurs.
        • Cependant, elle ne gère que l’identité (email, mot de passe, etc.).
        • Pour attribuer un rôle (ex. admin, auteur, lecteur), nous devons stocker des informations supplémentaires dans Cloud Firestore.
        • C’est donc la combinaison de Firebase Auth + Firestore qui permet une gestion complète des rôles et des autorisations.
      2. Structure générale du système d’authentification
          ComposantRôle
          Firebase Authentication Gère les identifiants (email, mot de passe, UID)
          Cloud Firestore Stocke les rôles et le statut de validation des utilisateurs
          AuthController Gère la logique d’inscription, de connexion, et de validation
          AuthWrapper Oriente l’utilisateur vers la bonne interface (admin, auteur, lecteur)
      3. Architecture du projet Flutter
          lib/ ├── controllers/ │ └── auth_controller.dart ← logique d'authentification ├── views/ │ ├── login_view.dart ← page de connexion │ ├── signup_view.dart ← page d’inscription │ ├── admin_dashboard_view.dart ← interface de l’admin │ ├── liste_livres_view.dart ← interface des auteurs/lecteurs │ └── pending_view.dart ← affichée si compte non validé └── main.dart ← redirection selon le rôle
      4. Fonctionnement général
        1. Lorsqu’un utilisateur s’inscrit

          • Son compte est créé dans Firebase Auth (createUserWithEmailAndPassword).
          • Son profil est ajouté dans Firestore avec :
          • {
              "email": "exemple@domaine.com",
              "role": "lecteur",
              "isApproved": false
            }
          • Il est déconnecté automatiquement.
          • L’admin doit valider son compte avant qu’il puisse se connecter.
        2. Lorsqu’un utilisateur se connecte

          • Firebase Auth vérifie l'email et le mot de passe.
          • L’application consulte Firestore pour vérifier :
          • s’il existe un profil correspondant à cet UID ;
          • si isApproved == true.
          • En fonction du rôle :
          • Admin → interface d’administration (AdminDashboardView)
          • Auteur ou Lecteur → interface principale (ListeLivresView)
          • Non validé → message d’attente de validation.
        3. Modèle UserModel étendu

            Fichier user_model.dart
            class UserModel {
              final String uid;
              final String email;
              final String role; // 'admin', 'auteur', 'lecteur'
              final bool isApproved; // true si l'utilisateur est approuvé
              final DateTime createdAt;
            
              UserModel({
                required this.uid,
                required this.email,
                required this.role,
                required this.isApproved,
                required this.createdAt,
              });
            
              factory UserModel.fromFirebaseUser(User user, {Map<String, dynamic>? additionalData}) {
                return UserModel(
                  uid: user.uid,
                  email: user.email ?? "",
                  role: additionalData?['role'] ?? 'lecteur', // Par défaut 'lecteur'
                  isApproved: additionalData?['isApproved'] ?? false, // Par défaut false
                  createdAt: additionalData?['createdAt']?.toDate() ?? DateTime.now(),
                );
              }
            
              Map<String, dynamic> toMap() {
                return {
                  'uid': uid,
                  'email': email,
                  'role': role,
                  'isApproved': isApproved,
                  'createdAt': createdAt,
                };
              }
            }
        4. Service Firestore pour gérer les utilisateurs

            Fichier user_service.dart
            import 'package:cloud_firestore/cloud_firestore.dart';
            
            class UserService {
              final FirebaseFirestore _firestore = FirebaseFirestore.instance;
            
              // Sauvegarder les données utilisateur dans Firestore
              Future<void> saveUserData(UserModel user) async {
                await _firestore.collection('users').doc(user.uid).set(user.toMap());
              }
            
              // Récupérer les données utilisateur
              Future<UserModel?> getUserData(String uid) async {
                final doc = await _firestore.collection('users').doc(uid).get();
                if (doc.exists) {
                  final data = doc.data()!;
                  return UserModel(
                    uid: uid,
                    email: data['email'],
                    role: data['role'],
                    isApproved: data['isApproved'],
                    createdAt: data['createdAt'].toDate(),
                  );
                }
                return null;
              }
            
              // Récupérer tous les utilisateurs en attente d'approbation
              Stream<List<UserModel>> getPendingUsers() {
                return _firestore
                    .collection('users')
                    .where('isApproved', isEqualTo: false)
                    .snapshots()
                    .map((snapshot) => snapshot.docs.map((doc) {
                          final data = doc.data();
                          return UserModel(
                            uid: doc.id,
                            email: data['email'],
                            role: data['role'],
                            isApproved: data['isApproved'],
                            createdAt: data['createdAt'].toDate(),
                          );
                        }).toList());
              }
            
              // Approuver un utilisateur
              Future<void> approveUser(String uid) async {
                await _firestore.collection('users').doc(uid).update({
                  'isApproved': true,
                });
              }
            
              // Refuser un utilisateur
              Future<void> rejectUser(String uid) async {
                await _firestore.collection('users').doc(uid).delete();
                // Optionnel : supprimer aussi le compte Firebase Auth
                // await FirebaseAuth.instance.deleteUser(uid);
              }
            }
        5. authController modifié

            Fichier auth_controller.dart
            class AuthController {
              final FirebaseAuth _auth = FirebaseAuth.instance;
              final GoogleSignIn _googleSignIn = GoogleSignIn(
                scopes: ['email', 'profile'],
              );
              final UserService _userService = UserService();
            
              // Inscription avec rôle
              Future register({
                required String email, 
                required String password, 
                required String role // 'auteur' ou 'lecteur'
              }) async {
                try {
                  if (role != 'auteur' && role != 'lecteur') {
                    return "Rôle invalide";
                  }
            
                  // Créer l'utilisateur Firebase
                  final userCredential = await _auth.createUserWithEmailAndPassword(
                    email: email.trim(),
                    password: password.trim(),
                  );
            
                  // Sauvegarder les données dans Firestore
                  final userModel = UserModel(
                    uid: userCredential.user!.uid,
                    email: email.trim(),
                    role: role,
                    isApproved: false, // Non approuvé par défaut
                    createdAt: DateTime.now(),
                  );
            
                  await _userService.saveUserData(userModel);
            
                  return null;
                } on FirebaseAuthException catch (e) {
                  return getErrorMessage(e);
                }
              }
            
              // Connexion avec vérification d'approbation
              Future login({required String email, required String password}) async {
                try {
                  final userCredential = await _auth.signInWithEmailAndPassword(
                    email: email.trim(),
                    password: password.trim(),
                  );
            
                  // Vérifier si l'utilisateur est approuvé
                  final userData = await _userService.getUserData(userCredential.user!.uid);
                  
                  if (userData == null) {
                    await _auth.signOut();
                    return "Compte non trouvé dans la base de données";
                  }
            
                  if (!userData.isApproved) {
                    await _auth.signOut();
                    return "Votre compte est en attente d'approbation par l'administrateur";
                  }
            
                  return null;
                } on FirebaseAuthException catch (e) {
                  return getErrorMessage(e);
                }
              }
            
              // Récupérer le UserModel complet
              Future<UserModel?> getCurrentUserModel() async {
                final user = _auth.currentUser;
                if (user != null) {
                  return await _userService.getUserData(user.uid);
                }
                return null;
              }
            
              // Stream pour écouter les changements d'état d'authentification ET d'approbation
              Stream get authStateChanges {
                return _auth.authStateChanges().asyncMap((user) async {
                  if (user != null) {
                    return await _userService.getUserData(user.uid);
                  }
                  return null;
                });
              }
            
              // ... reste du code existant (google signin, logout, etc.)
            }
        6. Vue d'inscription modifiée

            Fichier signup_view.dart
            class SignUpView extends StatefulWidget {
              const SignUpView({super.key});
            
              @override
              State createState() => _SignUpViewState();
            }
            
            class _SignUpViewState extends State {
              final auth = AuthController();
              final emailController = TextEditingController();
              final passwordController = TextEditingController();
              String? errorMessage;
              bool isLoading = false;
              String selectedRole = 'lecteur'; // Rôle par défaut
            
              Future register() async {
                setState(() { 
                  isLoading = true;
                  errorMessage = null;
                });
            
                final error = await auth.register(
                  email: emailController.text,
                  password: passwordController.text,
                  role: selectedRole,
                );
            
                if (error != null) {
                  setState(() => errorMessage = error);
                } else {
                  // Afficher un message de succès
                  showDialog(
                    context: context,
                    builder: (context) => AlertDialog(
                      title: const Text("Inscription réussie"),
                      content: const Text("Votre compte est en attente d'approbation par l'administrateur."),
                      actions: [
                        TextButton(
                          onPressed: () {
                            Navigator.pop(context); // Fermer la dialog
                            Navigator.pop(context); // Retour à la connexion
                          },
                          child: const Text("OK"),
                        ),
                      ],
                    ),
                  );
                }
            
                setState(() => isLoading = false);
              }
            
              @override
              Widget build(BuildContext context) {
                return Scaffold(
                  body: Container(
                    padding: const EdgeInsets.symmetric(horizontal: 25),
                    decoration: const BoxDecoration(
                      gradient: LinearGradient(
                        colors: [Color(0xFF11998E), Color(0xFF38EF7D)],
                        begin: Alignment.topLeft,
                        end: Alignment.bottomRight,
                      ),
                    ),
                    child: Center(
                      child: SingleChildScrollView(
                        child: Container(
                          padding: const EdgeInsets.all(25),
                          decoration: BoxDecoration(
                            color: Colors.white.withOpacity(0.95),
                            borderRadius: BorderRadius.circular(20),
                          ),
                          child: Column(
                            children: [
                              const Text(
                                "Créer un compte",
                                style: TextStyle(
                                  fontSize: 26,
                                  fontWeight: FontWeight.bold,
                                  color: Color(0xFF11998E),
                                ),
                              ),
                              const SizedBox(height: 25),
            
                              TextField(
                                controller: emailController,
                                decoration: InputDecoration(
                                  labelText: "Email",
                                  prefixIcon: const Icon(Icons.email_outlined),
                                  border: OutlineInputBorder(
                                    borderRadius: BorderRadius.circular(15),
                                  ),
                                ),
                              ),
            
                              const SizedBox(height: 15),
            
                              TextField(
                                controller: passwordController,
                                obscureText: true,
                                decoration: InputDecoration(
                                  labelText: "Mot de passe",
                                  prefixIcon: const Icon(Icons.lock_outline),
                                  border: OutlineInputBorder(
                                    borderRadius: BorderRadius.circular(15),
                                  ),
                                ),
                              ),
            
                              const SizedBox(height: 15),
            
                              // Sélection du rôle
                              DropdownButtonFormField(
                                value: selectedRole,
                                decoration: InputDecoration(
                                  labelText: "Rôle",
                                  prefixIcon: const Icon(Icons.person_outline),
                                  border: OutlineInputBorder(
                                    borderRadius: BorderRadius.circular(15),
                                  ),
                                ),
                                items: [
                                  DropdownMenuItem(
                                    value: 'lecteur',
                                    child: Text('Lecteur'),
                                  ),
                                  DropdownMenuItem(
                                    value: 'auteur',
                                    child: Text('Auteur'),
                                  ),
                                ],
                                onChanged: (value) {
                                  setState(() {
                                    selectedRole = value!;
                                  });
                                },
                              ),
            
                              const SizedBox(height: 20),
            
                              if (errorMessage != null)
                                Text(
                                  errorMessage!,
                                  style: const TextStyle(color: Colors.red),
                                  textAlign: TextAlign.center,
                                ),
            
                              const SizedBox(height: 20),
            
                              SizedBox(
                                width: double.infinity,
                                child: ElevatedButton(
                                  onPressed: isLoading ? null : register,
                                  style: ElevatedButton.styleFrom(
                                    padding: const EdgeInsets.symmetric(vertical: 12),
                                    backgroundColor: const Color(0xFF11998E),
                                    shape: RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(12),
                                    ),
                                  ),
                                  child: isLoading
                                      ? const CircularProgressIndicator(color: Colors.white)
                                      : const Text(
                                          "S'inscrire",
                                          style: TextStyle(fontSize: 18),
                                        ),
                                ),
                              ),
            
                              const SizedBox(height: 10),
            
                              TextButton(
                                onPressed: () => Navigator.pop(context),
                                child: const Text("Déjà un compte ? Se connecter"),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ),
                  ),
                );
              }
            }
        7. Vue Admin pour gérer les approbations

            Fichier admindashboard_view.dart
            class AdminDashboard extends StatelessWidget {
              final UserService _userService = UserService();
            
              AdminDashboard({super.key});
            
              @override
              Widget build(BuildContext context) {
                return Scaffold(
                  appBar: AppBar(
                    title: const Text("Tableau de bord Admin"),
                    backgroundColor: Colors.red,
                  ),
                  body: StreamBuilder>(
                    stream: _userService.getPendingUsers(),
                    builder: (context, snapshot) {
                      if (snapshot.connectionState == ConnectionState.waiting) {
                        return const Center(child: CircularProgressIndicator());
                      }
            
                      if (snapshot.hasError) {
                        return Center(child: Text('Erreur: ${snapshot.error}'));
                      }
            
                      final pendingUsers = snapshot.data ?? [];
            
                      if (pendingUsers.isEmpty) {
                        return const Center(child: Text("Aucune demande d'inscription en attente"));
                      }
            
                      return ListView.builder(
                        itemCount: pendingUsers.length,
                        itemBuilder: (context, index) {
                          final user = pendingUsers[index];
                          return Card(
                            margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                            child: ListTile(
                              title: Text(user.email),
                              subtitle: Text('Rôle: ${user.role} - Inscrit le: ${DateFormat('dd/MM/yyyy').format(user.createdAt)}'),
                              trailing: Row(
                                mainAxisSize: MainAxisSize.min,
                                children: [
                                  IconButton(
                                    icon: const Icon(Icons.check, color: Colors.green),
                                    onPressed: () async {
                                      await _userService.approveUser(user.uid);
                                    },
                                  ),
                                  IconButton(
                                    icon: const Icon(Icons.close, color: Colors.red),
                                    onPressed: () async {
                                      await _userService.rejectUser(user.uid);
                                    },
                                  ),
                                ],
                              ),
                            ),
                          );
                        },
                      );
                    },
                  ),
                );
              }
            }
        8. Modifications dans main.dart

            Fichier auth_controller.dart
            class MyApp extends StatelessWidget {
              const MyApp({super.key});
            
              @override
              Widget build(BuildContext context) {
                return MaterialApp(
                  home: StreamBuilder(
                    stream: AuthController().authStateChanges,
                    builder: (context, snapshot) {
                      if (snapshot.connectionState == ConnectionState.waiting) {
                        return const Scaffold(
                          body: Center(child: CircularProgressIndicator()),
                        );
                      }
            
                      final userModel = snapshot.data;
            
                      if (userModel == null) {
                        return const LoginView();
                      }
            
                      // Vérifier si l'utilisateur est approuvé
                      if (!userModel.isApproved) {
                        return const WaitingApprovalView();
                      }
            
                      // Rediriger selon le rôle
                      switch (userModel.role) {
                        case 'admin':
                          return const AdminDashboard();
                        case 'auteur':
                          return const AuthorDashboard();
                        case 'lecteur':
                          return const ReaderDashboard();
                        default:
                          return const LoginView();
                      }
                    },
                  ),
                );
              }
            }
            
            // Vue d'attente d'approbation
            class WaitingApprovalView extends StatelessWidget {
              const WaitingApprovalView({super.key});
            
              @override
              Widget build(BuildContext context) {
                return Scaffold(
                  body: Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Icon(Icons.schedule, size: 80, color: Colors.orange),
                        const SizedBox(height: 20),
                        const Text(
                          "En attente d'approbation",
                          style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                        ),
                        const SizedBox(height: 10),
                        const Text(
                          "Votre compte est en cours de validation par l'administrateur",
                          textAlign: TextAlign.center,
                          style: TextStyle(fontSize: 16),
                        ),
                        const SizedBox(height: 30),
                        ElevatedButton(
                          onPressed: () => AuthController().logout(),
                          child: const Text("Se déconnecter"),
                        ),
                      ],
                    ),
                  ),
                );
              }
            }
        9. Configuration Firestore

            Règles Firestore
            rules_version = '2';
            service cloud.firestore {
              match /databases/{database}/documents {
                match /users/{userId} {
                  allow read, write: if request.auth != null && 
                    (request.auth.uid == userId || 
                     get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin');
                }
              }
            }
        10. contrôleur d’authentification (auth_controller.dart)

            Fichier auth_controller.dart
            import 'package:cloud_firestore/cloud_firestore.dart';
            import 'package:firebase_auth/firebase_auth.dart';
            import 'package:flutter/material.dart';
            
            class AuthController with ChangeNotifier {
              final FirebaseAuth _auth = FirebaseAuth.instance;
              final FirebaseFirestore _firestore = FirebaseFirestore.instance;
            
              User? get currentUser => _auth.currentUser;
            
              // Inscription
              Future<String?> signUp(String email, String password, String role) async {
                try {
                  // Création du compte dans Firebase Authentication
                  UserCredential userCredential = await _auth.createUserWithEmailAndPassword(
                    email: email,
                    password: password,
                  );
            
                  // Ajout du profil utilisateur dans Firestore
                  await _firestore.collection('users').doc(userCredential.user!.uid).set({
                    'email': email,
                    'role': role,
                    'isApproved': false, //  Non validé par défaut
                    'createdAt': FieldValue.serverTimestamp(),
                  });
            
                  // Déconnexion automatique après l'inscription
                  await _auth.signOut();
            
                  return "Inscription réussie \nVotre compte est en attente de validation par l’administrateur.";
                } on FirebaseAuthException catch (e) {
                  if (e.code == 'email-already-in-use') {
                    return "Cet email est déjà utilisé.";
                  } else if (e.code == 'weak-password') {
                    return "Mot de passe trop faible.";
                  } else if (e.code == 'invalid-email') {
                    return "Adresse email invalide.";
                  }
                  return "Erreur d'authentification : ${e.message}";
                } catch (e) {
                  return "Erreur inattendue : $e";
                }
              }
            
              // Connexion
              Future<String?> login(String email, String password) async {
                try {
                  UserCredential userCredential =
                      await _auth.signInWithEmailAndPassword(email: email, password: password);
            
                  // Vérifie si l’utilisateur a un profil dans Firestore
                  final doc = await _firestore.collection('users').doc(userCredential.user!.uid).get();
            
                  if (!doc.exists) {
                    await _auth.signOut();
                    return "Profil utilisateur introuvable.";
                  }
            
                  final userData = doc.data();
            
                  if (userData == null || userData['isApproved'] == false) {
                    await _auth.signOut();
                    return "Votre compte est en attente de validation par l’administrateur.";
                  }
            
                  return null; //  Connexion réussie
                } on FirebaseAuthException catch (e) {
                  if (e.code == 'user-not-found') {
                    return "Aucun utilisateur trouvé avec cet email.";
                  } else if (e.code == 'wrong-password') {
                    return "Mot de passe incorrect.";
                  } else if (e.code == 'invalid-email') {
                    return "Adresse email invalide.";
                  }
                  return "Erreur d'authentification : ${e.message}";
                } catch (e) {
                  return "Erreur inattendue : $e";
                }
              }
            
              //  Déconnexion
              Future<void> logout() async {
                await _auth.signOut();
              }
            
              //  Écouteur d’état de connexion
              Stream<User?> authStateChanges() {
                return _auth.authStateChanges();
              }
            }
            
        11. Gestion de la redirection automatique (main.dart)

            Fichier main.dart
            import 'package:flutter/material.dart';
            import 'package:firebase_core/firebase_core.dart';
            import 'package:provider/provider.dart';
            import 'package:cloud_firestore/cloud_firestore.dart';
            import 'package:firebase_auth/firebase_auth.dart';
            
            import 'firebase_options.dart';
            import 'controllers/livre_controller.dart';
            import 'controllers/auth_controller.dart';
            import 'views/liste_livres_view_recherche.dart';
            import 'views/login_view.dart';
            import 'views/admin_dashboard_view.dart'; //  à ajouter
            
            void main() async {
              WidgetsFlutterBinding.ensureInitialized();
            
              await Firebase.initializeApp(
                options: DefaultFirebaseOptions.currentPlatform,
              );
            
              runApp(MyApp());
            }
            
            class MyApp extends StatelessWidget {
              @override
              Widget build(BuildContext context) {
                return MultiProvider(
                  providers: [
                    Provider<LivreController>(
                      create: (_) => LivreController(),
                    ),
                    ChangeNotifierProvider<AuthController>(
                      create: (_) => AuthController(),
                    ),
                  ],
                  child: MaterialApp(
                    title: 'Ma Bibliothèque',
                    theme: ThemeData(
                      useMaterial3: false,
                      primaryColor: const Color(0xFF4CAF50),
                    ),
                    debugShowCheckedModeBanner: false,
                    home: AuthWrapper(), //  redirection automatique selon le rôle
                  ),
                );
              }
            }
            
            class AuthWrapper extends StatelessWidget {
              const AuthWrapper({super.key});
            
              @override
              Widget build(BuildContext context) {
                return StreamBuilder<User?>(
                  stream: FirebaseAuth.instance.authStateChanges(),
                  builder: (context, snapshot) {
                    //  Chargement initial
                    if (snapshot.connectionState == ConnectionState.waiting) {
                      return const Scaffold(
                          body: Center(child: CircularProgressIndicator()));
                    }
            
                    //  Si pas connecté → page de connexion
                    if (!snapshot.hasData) {
                      return LoginView();
                    }
            
                    //  Utilisateur connecté
                    final user = snapshot.data!;
                    final usersRef = FirebaseFirestore.instance.collection('users');
            
                    return FutureBuilder<DocumentSnapshot>(
                      future: usersRef.doc(user.uid).get(),
                      builder: (context, userSnapshot) {
                        if (userSnapshot.connectionState == ConnectionState.waiting) {
                          return const Scaffold(
                              body: Center(child: CircularProgressIndicator()));
                        }
            
                        if (!userSnapshot.hasData || !userSnapshot.data!.exists) {
                          return const Scaffold(
                            body: Center(
                              child: Text(" Profil utilisateur introuvable."),
                            ),
                          );
                        }
            
                        final userData =
                            userSnapshot.data!.data() as Map<String, dynamic>? ?? {};
            
                        final role = userData['role'] ?? 'lecteur';
                        final isApproved = userData['isApproved'] ?? false;
            
                        //  Compte non validé
                        if (!isApproved) {
                          return const Scaffold(
                            body: Center(
                              child: Text(
                                " Votre compte est en attente de validation par l’administrateur.",
                                textAlign: TextAlign.center,
                                style: TextStyle(fontSize: 18),
                              ),
                            ),
                          );
                        }
            
                        //  Si admin → dashboard
                        if (role == 'admin') {
                          return const AdminDashboardView();
                        }
            
                        //  Sinon → page de livres
                        return const ListeLivresView();
                      },
                    );
                  },
                );
              }
            }
            
        12. Règles de sécurité Firestore

            Regles
            rules_version = '2';
            service cloud.firestore {
              match /databases/{database}/documents {
            
                match /users/{userId} {
                  //  Création (inscription) : tout utilisateur connecté peut créer son doc
                  allow create: if request.auth != null;
            
                  //  Lecture : 
                  //   - un utilisateur peut lire son propre profil
                  //   - ou un admin peut lire tous les profils
                  allow read: if request.auth != null && (
                    request.auth.uid == userId ||
                    get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin'
                  );
            
                  //  Mise à jour / suppression :
                  //   - un utilisateur peut modifier son propre profil
                  //   - un admin peut modifier/supprimer n’importe qui
                  allow update, delete: if request.auth != null && (
                    request.auth.uid == userId ||
                    get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin'
                  );
                }
            
                //  Interdire le reste
                match /{document=**} {
                  allow read, write: if false;
                }
              }
            }
            
  • Ajout d’un utilisateur et enregistrement simultané dans Firebase Authentication et Cloud Firestore

    1. Objectif pédagogique
      • À la fin de cette section, vous serez capable de :
      • Créer un utilisateur dans Firebase Authentication pour gérer son email et mot de passe.
      • Enregistrer ses informations complémentaires (rôle, état de validation, etc.) dans Cloud Firestore.
      • Comprendre pourquoi ces deux enregistrements sont nécessaires et comment ils interagissent.
    2. Pourquoi deux enregistrements ?
      • Firebase Authentication et Firestore ont des rôles bien distincts:
      • Service Rôle principal Exemple de données stockées
        Firebase Authentication Gère la sécurité, la connexion, la vérification d’identité Email, mot de passe, UID
        Cloud Firestore Stocke les informations métier liées à l’utilisateur Rôle (admin, auteur, lecteur), statut de validation, préférences, etc.
      • Ainsi, Auth garantit que l’utilisateur existe et peut se connecter,tandis que Firestore décrit qui il est et ce qu’il a le droit de faire dans l’application.
    3. Étapes du processus d’inscription
      • Lorsqu’un utilisateur s’inscrit, les opérations suivantes s’exécutent dans cet ordre :
      • Création du compte dans Firebase Authentication
        • → Auth crée un identifiant unique (UID) pour cet utilisateur.
      • Enregistrement dans Firestore
        • → On ajoute un document users/{uid} contenant des informations complémentaires :
          • email
          • role (auteur / lecteur)
          • isApproved (true/false pour validation par admin)
      • Déconnexion automatique après inscription
        • → L’utilisateur ne peut pas se connecter tant qu’il n’a pas été approuvé.

    Ressource PDF disponible

    Ce cours contient une version PDF téléchargeable. Le lien sera disponible prochainement.