From 9fb81f793bc944ed29a1345945f85b7172cdc06d Mon Sep 17 00:00:00 2001 From: Van Leemput Dayron Date: Wed, 1 Oct 2025 00:48:06 +0200 Subject: [PATCH] Implement login and signup test with json. --- ios/Podfile.lock | 7 + lib/models/user.dart | 84 +++ lib/pages/login.dart | 472 +++++++++++------ lib/pages/signup.dart | 495 +++++++++++------- lib/services/user_service.dart | 272 ++++++++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 32 ++ pubspec.yaml | 2 + 8 files changed, 1004 insertions(+), 362 deletions(-) create mode 100644 lib/models/user.dart create mode 100644 lib/services/user_service.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 244e8cd..761fc02 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,21 +1,28 @@ PODS: - Flutter (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS DEPENDENCIES: - Flutter (from `Flutter`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) EXTERNAL SOURCES: Flutter: :path: Flutter + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e diff --git a/lib/models/user.dart b/lib/models/user.dart new file mode 100644 index 0000000..4d74e44 --- /dev/null +++ b/lib/models/user.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; + +class User { + final String? id; + final String nom; + final String prenom; + final String email; + final String password; + + User({ + this.id, + required this.nom, + required this.prenom, + required this.email, + required this.password, + }); + + // Constructeur pour créer un User depuis un Map (utile pour Firebase) + factory User.fromMap(Map map) { + return User( + id: map['id'], + nom: map['nom'] ?? '', + prenom: map['prenom'] ?? '', + email: map['email'] ?? '', + password: map['password'] ?? '', + ); + } + + // Constructeur pour créer un User depuis JSON + factory User.fromJson(String jsonStr) { + Map map = json.decode(jsonStr); + return User.fromMap(map); + } + + // Méthode pour convertir un User en Map (utile pour Firebase) + Map toMap() { + return { + 'id': id, + 'nom': nom, + 'prenom': prenom, + 'email': email, + 'password': password, + }; + } + + // Méthode pour convertir un User en JSON + String toJson() { + return json.encode(toMap()); + } + + // Méthode pour obtenir le nom complet + String get fullName => '$prenom $nom'; + + // Méthode pour créer une copie avec des modifications + User copyWith({ + String? id, + String? nom, + String? prenom, + String? email, + String? password, + }) { + return User( + id: id ?? this.id, + nom: nom ?? this.nom, + prenom: prenom ?? this.prenom, + email: email ?? this.email, + password: password ?? this.password, + ); + } + + @override + String toString() { + return 'User(id: $id, nom: $nom, prenom: $prenom, email: $email)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is User && other.email == email; + } + + @override + int get hashCode => email.hashCode; +} diff --git a/lib/pages/login.dart b/lib/pages/login.dart index 18dd7f1..8b621c8 100644 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -1,207 +1,343 @@ -//import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import '../models/user.dart'; +import '../services/user_service.dart'; -class LoginPage extends StatelessWidget { +class LoginPage extends StatefulWidget { const LoginPage({super.key}); + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _userService = UserService(); + + bool _isLoading = false; + bool _obscurePassword = true; + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + // Validation de l'email + String? _validateEmail(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Email requis'; + } + final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + if (!emailRegex.hasMatch(value.trim())) { + return 'Email invalide'; + } + return null; + } + + // Validation du mot de passe + String? _validatePassword(String? value) { + if (value == null || value.isEmpty) { + return 'Mot de passe requis'; + } + return null; + } + + // Méthode de connexion + Future _login() async { + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() { + _isLoading = true; + }); + + try { + final user = await _userService.authenticateUser( + _emailController.text.trim(), + _passwordController.text, + ); + + if (user != null) { + // Connexion réussie + _showSuccessMessage('Connexion réussie !'); + + // Naviguer vers la page d'accueil + Navigator.pushReplacementNamed(context, '/home'); + } else { + // Échec de la connexion + _showErrorMessage('Email ou mot de passe incorrect'); + } + } catch (e) { + _showErrorMessage('Erreur lors de la connexion: ${e.toString()}'); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + void _showSuccessMessage(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.green, + duration: Duration(seconds: 2), + ), + ); + } + + void _showErrorMessage(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + duration: Duration(seconds: 3), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Center( - child: Column( - children: [ - const SizedBox(height: 40), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 40), - const Text( - 'Bienvenue sur Travel Mate', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - ), + // Titre + Text( + 'Travel Mate', + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), + ), - const SizedBox(height: 12), + const SizedBox(height: 12), - const Text( - 'Connectez-vous pour continuer', - style: TextStyle(fontSize: 16, color: Colors.grey), - ), + // Sous-titre + Text( + 'Connectez-vous pour continuer', + style: TextStyle(fontSize: 16, color: Colors.grey), + ), - const SizedBox(height: 80), + const SizedBox(height: 32), - const TextField( - decoration: InputDecoration( - labelText: 'Email', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), + // Champ email + TextFormField( + controller: _emailController, + validator: _validateEmail, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + labelText: 'Email', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: Icon(Icons.email), ), ), - ), - const SizedBox(height: 20), - const TextField( - obscureText: true, - decoration: InputDecoration( - labelText: 'Password', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - ), - ), + const SizedBox(height: 16), - const SizedBox(height: 20), - - ElevatedButton( - onPressed: () { - // Logique de connexion - Navigator.pushReplacementNamed(context, '/home'); - }, - style: ElevatedButton.styleFrom( - minimumSize: const Size.fromHeight(50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Text('Login', style: TextStyle(fontSize: 18)), - ), - const SizedBox(height: 20), - - TextButton( - onPressed: () { - Navigator.pushNamed(context, '/forgot'); - }, - child: const Text('Mot de passe oublié ?'), - ), - - const SizedBox(height: 20), - - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text("Pas encore inscrit ? "), - GestureDetector( - onTap: () { - // Logique d'inscription - Navigator.pushNamed(context, '/signup'); - }, - child: const Text( - 'Inscrivez-vous !', - style: TextStyle( - color: Color.fromARGB(255, 37, 109, 167), - decoration: TextDecoration.underline, + // Champ mot de passe + TextFormField( + controller: _passwordController, + validator: _validatePassword, + obscureText: _obscurePassword, + decoration: InputDecoration( + labelText: 'Mot de passe', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: Icon(Icons.lock), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility + : Icons.visibility_off, ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, ), ), - ], - ), + ), - const SizedBox(height: 40), + const SizedBox(height: 8), - Container( - width: double.infinity, - height: 1, - color: Colors.grey.shade300, - ), + // Lien "Mot de passe oublié" + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () { + Navigator.pushNamed(context, '/forgot'); + }, + child: Text('Mot de passe oublié?'), + ), + ), - const SizedBox(height: 40), + const SizedBox(height: 24), - Text( - 'Ou connectez-vous avec', - style: TextStyle(color: Colors.grey.shade600), - ), - - const SizedBox(height: 20), - - Row( - mainAxisAlignment: MainAxisAlignment.center, - - children: [ - Column( - children: [ - // Bouton Google avec fond - GestureDetector( - onTap: () { - // Logique de connexion avec Google - }, - child: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: Colors.black, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.3), - spreadRadius: 1, - blurRadius: 3, - offset: Offset(0, 1), - ), - ], - border: Border.all( - color: Colors.grey.shade300, - width: 1, - ), + // Bouton de connexion + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: _isLoading ? null : _login, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: _isLoading + ? CircularProgressIndicator(color: Colors.white) + : Text( + 'Se connecter', + style: TextStyle(fontSize: 16), ), - child: Center( - child: Image.asset( - 'assets/icons/google.png', - width: 24, - height: 24, + ), + ), + + const SizedBox(height: 24), + + // Lien d'inscription + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Vous n'avez pas de compte?"), + TextButton( + onPressed: () { + Navigator.pushNamed(context, '/signup'); + }, + child: Text('S\'inscrire'), + ), + ], + ), + + const SizedBox(height: 40), + + // Séparateur + Container( + width: double.infinity, + height: 1, + color: Colors.grey.shade300, + ), + + const SizedBox(height: 40), + + Text( + 'Ou connectez-vous avec', + style: TextStyle(color: Colors.grey.shade600), + ), + + const SizedBox(height: 20), + + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + // GOOGLE + GestureDetector( + onTap: () { + // TODO: Implémenter la connexion Google + _showErrorMessage( + 'Connexion Google non implémentée', + ); + }, + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.black, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.3), + spreadRadius: 1, + blurRadius: 3, + offset: Offset(0, 1), + ), + ], + border: Border.all( + color: Colors.grey.shade300, + width: 1, + ), + ), + child: Center( + child: Image.asset( + 'assets/icons/google.png', + width: 24, + height: 24, + ), ), ), ), - ), + const SizedBox(height: 8), + const Text('Google'), + ], + ), - const SizedBox(height: 8), - const Text('Google'), - ], - ), + const SizedBox(width: 40), - const SizedBox(width: 40), - - Column( - children: [ - // APPLE - GestureDetector( - onTap: () { - // Logique de connexion avec Google - }, - child: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: Colors.black, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.3), - spreadRadius: 1, - blurRadius: 3, - offset: Offset(0, 1), + Column( + children: [ + // APPLE + GestureDetector( + onTap: () { + // TODO: Implémenter la connexion Apple + _showErrorMessage( + 'Connexion Apple non implémentée', + ); + }, + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.black, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.3), + spreadRadius: 1, + blurRadius: 3, + offset: Offset(0, 1), + ), + ], + border: Border.all( + color: Colors.grey.shade300, + width: 1, ), - ], - border: Border.all( - color: Colors.grey.shade300, - width: 1, ), - ), - child: Center( - child: Image.asset( - 'assets/icons/apple_white.png', - width: 24, - height: 24, + child: Center( + child: Image.asset( + 'assets/icons/apple_white.png', + width: 24, + height: 24, + color: Colors.white, + ), ), ), ), - ), + const SizedBox(height: 8), + const Text('Apple'), + ], + ), + ], + ), - const SizedBox(height: 8), - const Text('Apple'), - ], - ), - ], - ), - ], + const SizedBox(height: 40), + ], + ), ), ), ), diff --git a/lib/pages/signup.dart b/lib/pages/signup.dart index 3f35a84..e80551e 100644 --- a/lib/pages/signup.dart +++ b/lib/pages/signup.dart @@ -1,8 +1,170 @@ import 'package:flutter/material.dart'; +import 'package:bcrypt/bcrypt.dart'; +import '../models/user.dart'; +import '../services/user_service.dart'; -class SignUpPage extends StatelessWidget { +class SignUpPage extends StatefulWidget { const SignUpPage({super.key}); + @override + State createState() => _SignUpPageState(); +} + +class _SignUpPageState extends State { + final _formKey = GlobalKey(); + final _nomController = TextEditingController(); + final _prenomController = TextEditingController(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + final _userService = UserService(); + + bool _isLoading = false; + bool _obscurePassword = true; + bool _obscureConfirmPassword = true; + + @override + void dispose() { + _nomController.dispose(); + _prenomController.dispose(); + _emailController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + // Méthode de validation + String? _validateField(String? value, String fieldName) { + if (value == null || value.trim().isEmpty) { + return '$fieldName est requis'; + } + return null; + } + + String? _validateEmail(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Email est requis'; + } + + final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + if (!emailRegex.hasMatch(value.trim())) { + return 'Email invalide'; + } + return null; + } + + String? _validatePassword(String? value) { + if (value == null || value.isEmpty) { + return 'Mot de passe est requis'; + } + if (value.length < 8) { + return 'Le mot de passe doit contenir au moins 8 caractères'; + } + return null; + } + + String? _validateConfirmPassword(String? value) { + if (value == null || value.isEmpty) { + return 'Confirmation du mot de passe requise'; + } + if (value != _passwordController.text) { + return 'Les mots de passe ne correspondent pas'; + } + return null; + } + + // Méthode d'enregistrement + Future _signUp() async { + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() { + _isLoading = true; + }); + + try { + // Vérifier si l'email existe déjà + bool emailExists = await _userService.emailExists( + _emailController.text.trim(), + ); + if (emailExists) { + _showErrorDialog('Cet email est déjà utilisé'); + return; + } + + // Hasher le mot de passe + String hashedPassword = BCrypt.hashpw( + _passwordController.text, + BCrypt.gensalt(), + ); + + // Créer l'utilisateur + User newUser = User( + nom: _nomController.text.trim(), + prenom: _prenomController.text.trim(), + email: _emailController.text.trim().toLowerCase(), + password: hashedPassword, + ); + + // Sauvegarder l'utilisateur + bool success = await _userService.addUser(newUser); + + if (success) { + _showSuccessDialog(); + } else { + _showErrorDialog('Erreur lors de la création du compte'); + } + } catch (e) { + _showErrorDialog('Une erreur est survenue: ${e.toString()}'); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + void _showSuccessDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Succès'), + content: Text('Votre compte a été créé avec succès !'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); // Fermer la dialog + Navigator.of(context).pop(); // Retourner à la page de login + }, + child: Text('OK'), + ), + ], + ); + }, + ); + } + + void _showErrorDialog(String message) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Erreur'), + content: Text(message), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text('OK'), + ), + ], + ); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -17,226 +179,171 @@ class SignUpPage extends StatelessWidget { elevation: 0, ), body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Center( - child: Column( - children: [ - const Text( - 'Bienvenue sur Travel Mate', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), - ), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + children: [ + const SizedBox(height: 40), - const SizedBox(height: 12), + const Text( + 'Créer un compte', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), - const Text( - 'Créez un compte pour continuer', - style: TextStyle(fontSize: 16, color: Colors.grey), - ), + const SizedBox(height: 12), - const SizedBox(height: 20), + const Text( + 'Rejoignez Travel Mate', + style: TextStyle(fontSize: 16, color: Colors.grey), + ), - const TextField( - decoration: InputDecoration( - labelText: 'Nom', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), + const SizedBox(height: 40), + + // Champ Nom + TextFormField( + controller: _nomController, + validator: (value) => _validateField(value, 'Nom'), + decoration: InputDecoration( + labelText: 'Nom', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: Icon(Icons.person), ), ), - ), - const SizedBox(height: 12), + const SizedBox(height: 20), - const TextField( - decoration: InputDecoration( - labelText: 'Prénom', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), + // Champ Prénom + TextFormField( + controller: _prenomController, + validator: (value) => _validateField(value, 'Prénom'), + decoration: InputDecoration( + labelText: 'Prénom', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: Icon(Icons.person_outline), ), ), - ), - const SizedBox(height: 20), + const SizedBox(height: 20), - const TextField( - decoration: InputDecoration( - labelText: 'Email', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), + // Champ Email + TextFormField( + controller: _emailController, + validator: _validateEmail, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + labelText: 'Email', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: Icon(Icons.email), ), ), - ), - const SizedBox(height: 20), + const SizedBox(height: 20), - const TextField( - obscureText: true, - decoration: InputDecoration( - labelText: 'Mot de passe', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - ), - ), - - const SizedBox(height: 20), - - const TextField( - obscureText: true, - decoration: InputDecoration( - labelText: 'Confirmez le mot de passe', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - ), - ), - - const SizedBox(height: 20), - - ElevatedButton( - onPressed: () { - // Logique d'inscription - Navigator.pushNamed(context, '/home'); - }, - style: ElevatedButton.styleFrom( - minimumSize: const Size.fromHeight(50), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Text( - 'S\'inscrire', - style: TextStyle(fontSize: 18), - ), - ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text("Déjà un compte ? "), - GestureDetector( - onTap: () { - // Logique de navigation vers la page de connexion - Navigator.pop(context); - }, - child: const Text( - 'Connectez-vous !', - style: TextStyle( - color: Colors.blue, - fontWeight: FontWeight.bold, + // Champ Mot de passe + TextFormField( + controller: _passwordController, + validator: _validatePassword, + obscureText: _obscurePassword, + decoration: InputDecoration( + labelText: 'Mot de passe', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: Icon(Icons.lock), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility + : Icons.visibility_off, ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, ), ), - ], - ), + ), - const SizedBox(height: 40), + const SizedBox(height: 20), - Container( - width: double.infinity, - height: 1, - color: Colors.grey.shade300, - ), + // Champ Confirmation mot de passe + TextFormField( + controller: _confirmPasswordController, + validator: _validateConfirmPassword, + obscureText: _obscureConfirmPassword, + decoration: InputDecoration( + labelText: 'Confirmez le mot de passe', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: Icon(Icons.lock_outline), + suffixIcon: IconButton( + icon: Icon( + _obscureConfirmPassword + ? Icons.visibility + : Icons.visibility_off, + ), + onPressed: () { + setState(() { + _obscureConfirmPassword = !_obscureConfirmPassword; + }); + }, + ), + ), + ), - const SizedBox(height: 40), + const SizedBox(height: 30), - Text( - 'Ou inscrivez-vous avec', - style: TextStyle(color: Colors.grey.shade600), - ), + // Bouton d'inscription + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: _isLoading ? null : _signUp, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: _isLoading + ? CircularProgressIndicator(color: Colors.white) + : Text('S\'inscrire', style: TextStyle(fontSize: 18)), + ), + ), - const SizedBox(height: 20), + const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.center, - - children: [ - Column( - children: [ - // Bouton Google avec fond - GestureDetector( - onTap: () { - // Logique de connexion avec Google - }, - child: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: Colors.black, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.3), - spreadRadius: 1, - blurRadius: 3, - offset: Offset(0, 1), - ), - ], - border: Border.all( - color: Colors.grey.shade300, - width: 1, - ), - ), - child: Center( - child: Image.asset( - 'assets/icons/google.png', - width: 24, - height: 24, - ), - ), + // Lien vers login + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Déjà un compte ? "), + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: const Text( + 'Connectez-vous !', + style: TextStyle( + color: Colors.blue, + fontWeight: FontWeight.bold, ), ), - - const SizedBox(height: 8), - const Text('Google'), - ], - ), - - const SizedBox(width: 40), - - Column( - children: [ - // APPLE - GestureDetector( - onTap: () { - // Logique de connexion avec Google - }, - child: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: Colors.black, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.3), - spreadRadius: 1, - blurRadius: 3, - offset: Offset(0, 1), - ), - ], - border: Border.all( - color: Colors.grey.shade300, - width: 1, - ), - ), - child: Center( - child: Image.asset( - 'assets/icons/apple_white.png', - width: 24, - height: 24, - ), - ), - ), - ), - - const SizedBox(height: 8), - const Text('Apple'), - ], - ), - ], - ), - ], + ), + ], + ), + ], + ), ), ), ), diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart new file mode 100644 index 0000000..59908a5 --- /dev/null +++ b/lib/services/user_service.dart @@ -0,0 +1,272 @@ +import 'dart:io'; +import 'dart:convert'; +import 'package:path_provider/path_provider.dart'; +import 'package:bcrypt/bcrypt.dart'; +import '../models/user.dart'; + +class UserService { + static const String _fileName = 'users.json'; + + // Obtenir le fichier JSON + Future _getUserFile() async { + final directory = await getApplicationDocumentsDirectory(); + return File('${directory.path}/$_fileName'); + } + + // Charger tous les utilisateurs + Future> loadUsers() async { + try { + final file = await _getUserFile(); + if (!await file.exists()) return []; + + final contents = await file.readAsString(); + if (contents.isEmpty) return []; + + final List jsonList = json.decode(contents); + + return jsonList.map((json) => User.fromMap(json)).toList(); + } catch (e) { + print('Erreur lors du chargement des utilisateurs: $e'); + return []; + } + } + + // Sauvegarder tous les utilisateurs + Future saveUsers(List users) async { + try { + final file = await _getUserFile(); + final jsonList = users.map((user) => user.toMap()).toList(); + await file.writeAsString(json.encode(jsonList)); + } catch (e) { + print('Erreur lors de la sauvegarde des utilisateurs: $e'); + throw Exception('Erreur de sauvegarde'); + } + } + + // Ajouter un nouvel utilisateur + Future addUser(User user) async { + try { + final users = await loadUsers(); + + // Vérifier si l'email existe déjà + if (users.any((u) => u.email.toLowerCase() == user.email.toLowerCase())) { + return false; // Email déjà utilisé + } + + // Générer un ID unique + final newUser = user.copyWith( + id: DateTime.now().millisecondsSinceEpoch.toString(), + ); + + users.add(newUser); + await saveUsers(users); + return true; + } catch (e) { + print('Erreur lors de l\'ajout de l\'utilisateur: $e'); + return false; + } + } + + // Authentifier un utilisateur avec bcrypt + Future authenticateUser(String email, String password) async { + try { + final users = await loadUsers(); + + // Trouver l'utilisateur par email (insensible à la casse) + User? user; + try { + user = users.firstWhere( + (u) => u.email.toLowerCase() == email.toLowerCase(), + ); + } catch (e) { + return null; // Utilisateur non trouvé + } + + // Vérifier le mot de passe avec bcrypt + if (BCrypt.checkpw(password, user.password)) { + return user; + } + + return null; // Mot de passe incorrect + } catch (e) { + print('Erreur lors de l\'authentification: $e'); + return null; + } + } + + // Vérifier si un email existe + Future emailExists(String email) async { + try { + final users = await loadUsers(); + return users.any( + (user) => user.email.toLowerCase() == email.toLowerCase(), + ); + } catch (e) { + print('Erreur lors de la vérification de l\'email: $e'); + return false; + } + } + + // Obtenir un utilisateur par ID + Future getUserById(String id) async { + try { + final users = await loadUsers(); + return users.firstWhere((user) => user.id == id); + } catch (e) { + print('Utilisateur avec l\'ID $id non trouvé'); + return null; + } + } + + // Obtenir un utilisateur par email + Future getUserByEmail(String email) async { + try { + final users = await loadUsers(); + return users.firstWhere( + (user) => user.email.toLowerCase() == email.toLowerCase(), + ); + } catch (e) { + print('Utilisateur avec l\'email $email non trouvé'); + return null; + } + } + + // Mettre à jour un utilisateur + Future updateUser(User updatedUser) async { + try { + final users = await loadUsers(); + final index = users.indexWhere((user) => user.id == updatedUser.id); + + if (index != -1) { + users[index] = updatedUser; + await saveUsers(users); + return true; + } + return false; // Utilisateur non trouvé + } catch (e) { + print('Erreur lors de la mise à jour de l\'utilisateur: $e'); + return false; + } + } + + // Supprimer un utilisateur + Future deleteUser(String id) async { + try { + final users = await loadUsers(); + final initialLength = users.length; + users.removeWhere((user) => user.id == id); + + if (users.length < initialLength) { + await saveUsers(users); + return true; + } + return false; // Utilisateur non trouvé + } catch (e) { + print('Erreur lors de la suppression de l\'utilisateur: $e'); + return false; + } + } + + // Changer le mot de passe d'un utilisateur + Future changePassword( + String userId, + String oldPassword, + String newPassword, + ) async { + try { + final users = await loadUsers(); + final userIndex = users.indexWhere((user) => user.id == userId); + + if (userIndex == -1) return false; // Utilisateur non trouvé + + final user = users[userIndex]; + + // Vérifier l'ancien mot de passe + if (!BCrypt.checkpw(oldPassword, user.password)) { + return false; // Ancien mot de passe incorrect + } + + // Hasher le nouveau mot de passe + final hashedNewPassword = BCrypt.hashpw(newPassword, BCrypt.gensalt()); + + // Mettre à jour l'utilisateur + users[userIndex] = user.copyWith(password: hashedNewPassword); + await saveUsers(users); + + return true; + } catch (e) { + print('Erreur lors du changement de mot de passe: $e'); + return false; + } + } + + // Réinitialiser le mot de passe (pour la fonctionnalité "mot de passe oublié") + Future resetPassword(String email, String newPassword) async { + try { + final users = await loadUsers(); + final userIndex = users.indexWhere( + (user) => user.email.toLowerCase() == email.toLowerCase(), + ); + + if (userIndex == -1) return false; // Utilisateur non trouvé + + // Hasher le nouveau mot de passe + final hashedNewPassword = BCrypt.hashpw(newPassword, BCrypt.gensalt()); + + // Mettre à jour l'utilisateur + users[userIndex] = users[userIndex].copyWith(password: hashedNewPassword); + await saveUsers(users); + + return true; + } catch (e) { + print('Erreur lors de la réinitialisation du mot de passe: $e'); + return false; + } + } + + // Obtenir le nombre total d'utilisateurs + Future getUserCount() async { + try { + final users = await loadUsers(); + return users.length; + } catch (e) { + print('Erreur lors du comptage des utilisateurs: $e'); + return 0; + } + } + + // Vider la base de données (utile pour les tests) + Future clearAllUsers() async { + try { + await saveUsers([]); + } catch (e) { + print('Erreur lors du vidage de la base de données: $e'); + } + } + + // Méthode pour créer des utilisateurs de test + Future createTestUsers() async { + try { + final testUsers = [ + User( + nom: 'Dupont', + prenom: 'Jean', + email: 'jean.dupont@test.com', + password: BCrypt.hashpw('password123', BCrypt.gensalt()), + ), + User( + nom: 'Martin', + prenom: 'Marie', + email: 'marie.martin@test.com', + password: BCrypt.hashpw('password123', BCrypt.gensalt()), + ), + ]; + + for (final user in testUsers) { + await addUser(user); + } + } catch (e) { + print('Erreur lors de la création des utilisateurs de test: $e'); + } + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 724bb2a..b8e2b22 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,10 @@ import FlutterMacOS import Foundation +import path_provider_foundation import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 80776f2..4ad9f61 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bcrypt: + dependency: "direct main" + description: + name: bcrypt + sha256: "9dc3f234d5935a76917a6056613e1a6d9b53f7fa56f98e24cd49b8969307764b" + url: "https://pub.dev" + source: hosted + version: "1.1.3" boolean_selector: dependency: transitive description: @@ -168,6 +176,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" + url: "https://pub.dev" + source: hosted + version: "2.2.18" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.dev" + source: hosted + version: "2.4.2" path_provider_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 432e115..a455f72 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,6 +32,8 @@ dependencies: sdk: flutter provider: ^6.1.1 shared_preferences: ^2.2.2 + path_provider: ^2.1.1 + bcrypt: ^1.1.3 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.