feat(frontend): simulate authenticated user sessions

This commit is contained in:
SaidSoighiri94
2026-07-23 12:39:12 +02:00
parent 313bb1c6ef
commit 530bd52cb5
6 changed files with 104 additions and 54 deletions
+7 -1
View File
@@ -1,6 +1,7 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:google_fonts/google_fonts.dart"; import "package:google_fonts/google_fonts.dart";
import "../demo_identity.dart"; import "../demo_identity.dart";
import "../services/auth_service.dart";
import "../theme/ensup_colors.dart"; import "../theme/ensup_colors.dart";
import "../widgets/ensup_top_bar.dart"; import "../widgets/ensup_top_bar.dart";
import "../widgets/profile_card.dart"; import "../widgets/profile_card.dart";
@@ -13,6 +14,11 @@ import "restitution_select_screen.dart";
class HomeScreen extends StatelessWidget { class HomeScreen extends StatelessWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
void _quitter(BuildContext context) {
AuthService.clearSession();
Navigator.of(context).pop();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -42,7 +48,7 @@ class HomeScreen extends StatelessWidget {
campusTag: DemoIdentity.campusTag, campusTag: DemoIdentity.campusTag,
userName: DemoIdentity.userName, userName: DemoIdentity.userName,
userInitials: DemoIdentity.userInitials, userInitials: DemoIdentity.userInitials,
onBack: () => Navigator.of(context).pop(), onBack: () => _quitter(context),
), ),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
+13 -9
View File
@@ -1,6 +1,7 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:google_fonts/google_fonts.dart"; import "package:google_fonts/google_fonts.dart";
import "../demo_identity.dart"; import "../demo_identity.dart";
import "../services/auth_service.dart";
import "../theme/ensup_colors.dart"; import "../theme/ensup_colors.dart";
import "../widgets/ensup_top_bar.dart"; import "../widgets/ensup_top_bar.dart";
import "../widgets/identification_option.dart"; import "../widgets/identification_option.dart";
@@ -13,6 +14,7 @@ class LoginScreen extends StatelessWidget {
const LoginScreen({super.key}); const LoginScreen({super.key});
void _identifier(BuildContext context) { void _identifier(BuildContext context) {
AuthService.selectDemoStudent();
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => const HomeScreen(), builder: (_) => const HomeScreen(),
@@ -21,6 +23,16 @@ class LoginScreen extends StatelessWidget {
); );
} }
void _identifierResponsable(BuildContext context) {
AuthService.selectDemoResponsable();
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const ResponsableScreen(),
settings: const RouteSettings(name: "responsable"),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -162,15 +174,7 @@ class LoginScreen extends StatelessWidget {
width: double.infinity, width: double.infinity,
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: () => onPressed: () =>
Navigator.of(context).push( _identifierResponsable(context),
MaterialPageRoute(
builder: (_) =>
const ResponsableScreen(),
settings: const RouteSettings(
name: "responsable",
),
),
),
icon: const Icon( icon: const Icon(
Icons.person_outline, Icons.person_outline,
size: 16, size: 16,
@@ -2,6 +2,7 @@ import "package:flutter/material.dart";
import "package:google_fonts/google_fonts.dart"; import "package:google_fonts/google_fonts.dart";
import "../demo_identity.dart"; import "../demo_identity.dart";
import "../services/auth_service.dart";
import "../services/responsable_service.dart"; import "../services/responsable_service.dart";
import "../theme/ensup_colors.dart"; import "../theme/ensup_colors.dart";
import "../utils/file_download.dart"; import "../utils/file_download.dart";
@@ -44,6 +45,11 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
setState(() => _future = _charger()); setState(() => _future = _charger());
} }
void _quitter() {
AuthService.clearSession();
Navigator.of(context).pop();
}
Future<void> _changerStatut(Map<String, dynamic> anomalie) async { Future<void> _changerStatut(Map<String, dynamic> anomalie) async {
final prochain = _prochainStatut(anomalie["statut"] as String?); final prochain = _prochainStatut(anomalie["statut"] as String?);
if (prochain == null || _transitionEnCours) { if (prochain == null || _transitionEnCours) {
@@ -124,10 +130,7 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
_ResponsableTopBar( _ResponsableTopBar(onExit: _quitter, onRefresh: _rafraichir),
onExit: () => Navigator.of(context).pop(),
onRefresh: _rafraichir,
),
Expanded( Expanded(
child: FutureBuilder<_ResponsableData>( child: FutureBuilder<_ResponsableData>(
future: _future, future: _future,
@@ -249,7 +252,7 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
anomaliesActives: anomaliesActives, anomaliesActives: anomaliesActives,
notificationsNonLues: notificationsNonLues, notificationsNonLues: notificationsNonLues,
onChanged: (index) => setState(() => _onglet = index), onChanged: (index) => setState(() => _onglet = index),
onExit: () => Navigator.of(context).pop(), onExit: _quitter,
), ),
Expanded( Expanded(
child: Column( child: Column(
+10 -29
View File
@@ -1,6 +1,7 @@
import "dart:convert"; import "dart:convert";
import "dart:typed_data"; import "dart:typed_data";
import "package:http/http.dart" as http; import "package:http/http.dart" as http;
import "auth_service.dart";
/// Erreur levée par le client API (serveur injoignable ou réponse en échec). /// Erreur levée par le client API (serveur injoignable ou réponse en échec).
class ApiException implements Exception { class ApiException implements Exception {
@@ -24,27 +25,19 @@ class ApiFile {
final String contentType; final String contentType;
} }
/// Point d'accès unique au backend : centralise l'URL de base, l'en-tête /// Point d'accès unique au backend : centralise l'URL, l'authentification et
/// d'authentification (simulée) et la gestion des erreurs. /// la gestion des erreurs.
class ApiClient { class ApiClient {
ApiClient._(); ApiClient._();
static const String _baseUrl = "http://localhost:3000/api"; static const String _baseUrl = "http://localhost:3000/api";
static const String _utilisateurEmail = "lucas.martin@ensitech.eu";
static const String responsableEmail = "karim.benali@ensup.eu";
static Future<dynamic> get( static Future<dynamic> get(String chemin) async {
String chemin, {
String utilisateurEmail = _utilisateurEmail,
}) async {
final uri = Uri.parse("$_baseUrl$chemin"); final uri = Uri.parse("$_baseUrl$chemin");
late final http.Response reponse; late final http.Response reponse;
try { try {
reponse = await http.get( reponse = await http.get(uri, headers: AuthService.requestHeaders());
uri,
headers: {"x-user-email": utilisateurEmail},
);
} catch (_) { } catch (_) {
throw const ApiException( throw const ApiException(
"Impossible de joindre le serveur. Vérifiez que le backend est démarré.", "Impossible de joindre le serveur. Vérifiez que le backend est démarré.",
@@ -60,16 +53,12 @@ class ApiClient {
static Future<ApiFile> getFile( static Future<ApiFile> getFile(
String chemin, { String chemin, {
required String fallbackFileName, required String fallbackFileName,
String utilisateurEmail = _utilisateurEmail,
}) async { }) async {
final uri = Uri.parse("$_baseUrl$chemin"); final uri = Uri.parse("$_baseUrl$chemin");
late final http.Response reponse; late final http.Response reponse;
try { try {
reponse = await http.get( reponse = await http.get(uri, headers: AuthService.requestHeaders());
uri,
headers: {"x-user-email": utilisateurEmail},
);
} catch (_) { } catch (_) {
throw const ApiException( throw const ApiException(
"Impossible de joindre le serveur. Vérifiez que le backend est démarré.", "Impossible de joindre le serveur. Vérifiez que le backend est démarré.",
@@ -88,11 +77,7 @@ class ApiClient {
); );
} }
static Future<dynamic> post( static Future<dynamic> post(String chemin, Map<String, dynamic> body) async {
String chemin,
Map<String, dynamic> body, {
String utilisateurEmail = _utilisateurEmail,
}) async {
final uri = Uri.parse("$_baseUrl$chemin"); final uri = Uri.parse("$_baseUrl$chemin");
late final http.Response reponse; late final http.Response reponse;
@@ -100,8 +85,8 @@ class ApiClient {
reponse = await http.post( reponse = await http.post(
uri, uri,
headers: { headers: {
...AuthService.requestHeaders(),
"content-type": "application/json", "content-type": "application/json",
"x-user-email": utilisateurEmail,
}, },
body: jsonEncode(body), body: jsonEncode(body),
); );
@@ -117,11 +102,7 @@ class ApiClient {
throw ApiException(_messageErreur(reponse)); throw ApiException(_messageErreur(reponse));
} }
static Future<dynamic> patch( static Future<dynamic> patch(String chemin, Map<String, dynamic> body) async {
String chemin,
Map<String, dynamic> body, {
String utilisateurEmail = _utilisateurEmail,
}) async {
final uri = Uri.parse("$_baseUrl$chemin"); final uri = Uri.parse("$_baseUrl$chemin");
late final http.Response reponse; late final http.Response reponse;
@@ -129,8 +110,8 @@ class ApiClient {
reponse = await http.patch( reponse = await http.patch(
uri, uri,
headers: { headers: {
...AuthService.requestHeaders(),
"content-type": "application/json", "content-type": "application/json",
"x-user-email": utilisateurEmail,
}, },
body: jsonEncode(body), body: jsonEncode(body),
); );
@@ -0,0 +1,63 @@
enum AuthenticationMode { demo, azure }
class AuthService {
AuthService._();
static const String _configuredMode = String.fromEnvironment(
"AUTH_MODE",
defaultValue: "demo",
);
static const String azureTenantId = String.fromEnvironment("AZURE_TENANT_ID");
static const String azureSpaClientId = String.fromEnvironment(
"AZURE_SPA_CLIENT_ID",
);
static const String azureApiScope = String.fromEnvironment("AZURE_API_SCOPE");
static const String demoStudentEmail = "lucas.martin@ensitech.eu";
static const String demoResponsableEmail = "karim.benali@ensup.eu";
static String? _demoEmail;
static AuthenticationMode get mode {
return switch (_configuredMode) {
"demo" => AuthenticationMode.demo,
"azure" => AuthenticationMode.azure,
_ => throw StateError("AUTH_MODE doit valoir demo ou azure."),
};
}
static void selectDemoStudent() {
_ensureDemoMode();
_demoEmail = demoStudentEmail;
}
static void selectDemoResponsable() {
_ensureDemoMode();
_demoEmail = demoResponsableEmail;
}
static void clearSession() {
_demoEmail = null;
}
static Map<String, String> requestHeaders() {
if (mode == AuthenticationMode.azure) {
throw StateError("La session Microsoft n'est pas encore initialisee.");
}
final email = _demoEmail;
if (email == null) {
throw StateError("Aucun utilisateur de demonstration selectionne.");
}
return {"x-user-email": email};
}
static void _ensureDemoMode() {
if (mode != AuthenticationMode.demo) {
throw StateError(
"Une identite de demonstration est interdite en mode Azure.",
);
}
}
}
@@ -4,13 +4,8 @@ import "api_client.dart";
class ResponsableService { class ResponsableService {
ResponsableService._(); ResponsableService._();
static const String _email = ApiClient.responsableEmail;
static Future<Map<String, dynamic>> dashboard() async { static Future<Map<String, dynamic>> dashboard() async {
final reponse = await ApiClient.get( final reponse = await ApiClient.get("/responsable/dashboard");
"/responsable/dashboard",
utilisateurEmail: _email,
);
return Map<String, dynamic>.from(reponse["data"] as Map); return Map<String, dynamic>.from(reponse["data"] as Map);
} }
@@ -32,14 +27,12 @@ class ResponsableService {
static Future<ApiFile> exporterHistorique() => ApiClient.getFile( static Future<ApiFile> exporterHistorique() => ApiClient.getFile(
"/responsable/historique/export.csv", "/responsable/historique/export.csv",
fallbackFileName: "historique-responsable.csv", fallbackFileName: "historique-responsable.csv",
utilisateurEmail: _email,
); );
static Future<int> marquerNotificationsLues() async { static Future<int> marquerNotificationsLues() async {
final reponse = await ApiClient.patch( final reponse = await ApiClient.patch(
"/responsable/notifications/lu-toutes", "/responsable/notifications/lu-toutes",
{}, {},
utilisateurEmail: _email,
); );
final data = Map<String, dynamic>.from(reponse["data"] as Map); final data = Map<String, dynamic>.from(reponse["data"] as Map);
return data["count"] as int; return data["count"] as int;
@@ -52,12 +45,12 @@ class ResponsableService {
final reponse = await ApiClient.patch("/responsable/anomalies/$id/statut", { final reponse = await ApiClient.patch("/responsable/anomalies/$id/statut", {
"statut": statut, "statut": statut,
"observation": "Traitement depuis l'interface responsable", "observation": "Traitement depuis l'interface responsable",
}, utilisateurEmail: _email); });
return Map<String, dynamic>.from(reponse["data"] as Map); return Map<String, dynamic>.from(reponse["data"] as Map);
} }
static Future<List<Map<String, dynamic>>> _liste(String chemin) async { static Future<List<Map<String, dynamic>>> _liste(String chemin) async {
final reponse = await ApiClient.get(chemin, utilisateurEmail: _email); final reponse = await ApiClient.get(chemin);
final data = reponse["data"] as List; final data = reponse["data"] as List;
return data.map((item) => Map<String, dynamic>.from(item as Map)).toList(); return data.map((item) => Map<String, dynamic>.from(item as Map)).toList();
} }