feat(auth): connect Flutter login to MSAL

This commit is contained in:
SaidSoighiri94
2026-07-23 14:13:58 +02:00
parent ef31e2ab8c
commit dc208fa13a
3 changed files with 126 additions and 16 deletions
+12 -3
View File
@@ -14,10 +14,19 @@ import "restitution_select_screen.dart";
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
void _quitter(BuildContext context) {
AuthService.clearSession();
Future<void> _quitter(BuildContext context) async {
try {
await AuthService.signOut();
if (context.mounted) {
Navigator.of(context).pop();
}
} catch (error) {
if (!context.mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(error.toString())));
}
}
@override
Widget build(BuildContext context) {
@@ -48,7 +57,7 @@ class HomeScreen extends StatelessWidget {
campusTag: DemoIdentity.campusTag,
userName: DemoIdentity.userName,
userInitials: DemoIdentity.userInitials,
onBack: () => _quitter(context),
onBack: () async => _quitter(context),
),
Expanded(
child: SingleChildScrollView(
+101 -9
View File
@@ -1,6 +1,7 @@
import "package:flutter/material.dart";
import "package:google_fonts/google_fonts.dart";
import "../demo_identity.dart";
import "../services/api_client.dart";
import "../services/auth_service.dart";
import "../theme/ensup_colors.dart";
import "../widgets/ensup_top_bar.dart";
@@ -10,11 +11,17 @@ import "home_screen.dart";
import "responsable_screen.dart";
/// Écran de démarrage : identification de l'étudiant (RG01/RG02).
class LoginScreen extends StatelessWidget {
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
void _identifier(BuildContext context) {
AuthService.selectDemoStudent();
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
bool _connexionEnCours = false;
void _ouvrirEtudiant() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const HomeScreen(),
@@ -23,8 +30,7 @@ class LoginScreen extends StatelessWidget {
);
}
void _identifierResponsable(BuildContext context) {
AuthService.selectDemoResponsable();
void _ouvrirResponsable() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const ResponsableScreen(),
@@ -33,6 +39,83 @@ class LoginScreen extends StatelessWidget {
);
}
void _identifierParQr() {
if (_connexionEnCours) return;
if (AuthService.mode == AuthenticationMode.azure) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
"L'identification QR securisee n'est pas encore configuree.",
),
),
);
return;
}
AuthService.selectDemoStudent();
_ouvrirEtudiant();
}
Future<void> _connecterMicrosoft({required bool responsableDemo}) async {
if (_connexionEnCours) return;
setState(() => _connexionEnCours = true);
try {
if (AuthService.mode == AuthenticationMode.demo) {
if (responsableDemo) {
AuthService.selectDemoResponsable();
_ouvrirResponsable();
} else {
AuthService.selectDemoStudent();
_ouvrirEtudiant();
}
return;
}
await AuthService.signInWithMicrosoft();
final reponse = await ApiClient.get("/auth/me");
final roleCode = _roleCode(reponse);
if (!mounted) return;
switch (roleCode) {
case "ETUDIANT":
_ouvrirEtudiant();
case "RESPONSABLE":
_ouvrirResponsable();
default:
throw StateError("Role utilisateur non autorise.");
}
} catch (error) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(error.toString())));
} finally {
if (mounted) {
setState(() => _connexionEnCours = false);
}
}
}
String _roleCode(dynamic reponse) {
if (reponse is! Map || reponse["data"] is! Map) {
throw StateError("Profil utilisateur invalide.");
}
final data = reponse["data"] as Map;
if (data["role"] is! Map) {
throw StateError("Role utilisateur absent.");
}
final role = data["role"] as Map;
final code = role["code"];
if (code is! String) {
throw StateError("Role utilisateur invalide.");
}
return code;
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -134,7 +217,9 @@ class LoginScreen extends StatelessWidget {
label: "Scanner ma carte étudiante",
description:
"Présentez votre carte étudiante dématérialisée à la douchette ou à la tablette",
onTap: () => _identifier(context),
onTap: _connexionEnCours
? null
: _identifierParQr,
),
const SizedBox(height: 12),
IdentificationOption(
@@ -144,7 +229,11 @@ class LoginScreen extends StatelessWidget {
"Connexion compte professionnel ENSUP",
description:
"Utiliser mon adresse e-mail Microsoft 365",
onTap: () => _identifier(context),
onTap: _connexionEnCours
? null
: () => _connecterMicrosoft(
responsableDemo: false,
),
),
const SizedBox(height: 20),
Row(
@@ -173,8 +262,11 @@ class LoginScreen extends StatelessWidget {
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () =>
_identifierResponsable(context),
onPressed: _connexionEnCours
? null
: () => _connecterMicrosoft(
responsableDemo: true,
),
icon: const Icon(
Icons.person_outline,
size: 16,
@@ -45,10 +45,19 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
setState(() => _future = _charger());
}
void _quitter() {
AuthService.clearSession();
Future<void> _quitter() async {
try {
await AuthService.signOut();
if (mounted) {
Navigator.of(context).pop();
}
} catch (error) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(error.toString())));
}
}
Future<void> _changerStatut(Map<String, dynamic> anomalie) async {
final prochain = _prochainStatut(anomalie["statut"] as String?);