Implémenter la Gestion des Rôles Firebase avec Flutter
Implémenter la Gestion des Rôles Firebase avec Flutter
-
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.
-
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. -
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. -
Architecture du projet (MVC)
- Le projet est organisé selon le pattern MVC (Modèle – Vue – Contrôleur) avec la structure suivante :
-
Configuration du projet
-
É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
-
É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.dartdanslib/. -
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 :
- c. Modifier android/app/build.gradle :
- d. Modifier le fichier : android/app/build.gradle.kts
- Dans ton fichier
android/app/build.gradle.kts, ajoute (ou modifie) ce bloc dans android : - e. Ajouter les dépendances dans pubspec.yaml :
- 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 : -
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
nullaprès la connexion. -
Étape 1 : Créer le compte dans Firebase Authentication
- Allez sur console.firebase.google.com
- Votre projet → Authentication → Users → Add 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 -
É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
users→ Add document - Dans Document ID, collez l’UID exact copié à l’étape 1
- Ajoutez les champs suivants :
- ⚠️ La valeur du champ
roledoit être exactement"admin"(en minuscule), c’est la valeur lue parUserRoleExtension.fromString()dans le modèle. -
Pourquoi ces deux étapes sont-elles nécessaires ?
- 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.
-
Fichiers réalisés
-
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 :
-
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 :
-
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 :
-
Fichier
main.dart -
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 :
-
Fichier
views/admin/admin_dashboard.dart - Le dashboard admin comporte deux onglets : création de comptes et liste des utilisateurs :
-
Fichier
views/enseignant/enseignant_dashboard.dart - L’enseignant est propriétaire de ses cours. Ses cours sont filtrés par son UID dans Firestore :
-
Fichier
views/etudiant/etudiant_dashboard.dart - L’étudiant a un accès en lecture seule sur les cours. Il peut rechercher et s’inscrire :
-
Fichier
views/responsable/responsable_dashboard.dart - Le responsable étudiant a une vue de supervision en lecture seule sur les étudiants et les cours :
-
Fichier
firestore.rules— Security Rules - Copiez ces règles dans Firebase Console → Firestore → Règles pour sécuriser l’accès côté serveur :
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
buildscript {
dependencies {
classpath("com.google.gms:google-services:4.3.15")
}
repositories {
google()
mavenCentral()
}
}
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
}
android {
ndkVersion = "27.0.12077973"
// ... le reste de ta configuration Android
}
dependencies:
flutter:
sdk: flutter
firebase_core: ^2.24.2
firebase_auth: ^4.16.0
cloud_firestore: ^4.14.0
{
"email": "admin@univ.tn",
"nom": "Admin123",
"prenom": "Super",
"role": "admin",
"createdAt": [timestamp actuel]
}
{
"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
}
Firebase Auth → vérifie email + mot de passe ✅
Firestore → stocke le rôle "admin" ✅
↓
AuthController.signIn() lit le champ "role" dans Firestore
↓
_redirectByRole() → AdminDashboard 🔴
// 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';
}
// 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}';
}
}
}
// 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();
}
}
// 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() },
);
}
}
// 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'),
),
],
),
),
),
),
),
),
);
}
}
// 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();
},
),
);
},
);
}
}
// 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();
},
),
);
},
),
],
),
);
}
}
// 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"),
),
),
);
},
),
),
]),
);
}
}
// 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'] ?? ''}'),
);
},
),
],
),
);
}
}
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);
}
}
}
