Implement login and signup test with json.
This commit is contained in:
84
lib/models/user.dart
Normal file
84
lib/models/user.dart
Normal file
@@ -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<String, dynamic> 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<String, dynamic> map = json.decode(jsonStr);
|
||||
return User.fromMap(map);
|
||||
}
|
||||
|
||||
// Méthode pour convertir un User en Map (utile pour Firebase)
|
||||
Map<String, dynamic> 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;
|
||||
}
|
||||
@@ -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<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
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<void> _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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<SignUpPage> createState() => _SignUpPageState();
|
||||
}
|
||||
|
||||
class _SignUpPageState extends State<SignUpPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
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<void> _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'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
272
lib/services/user_service.dart
Normal file
272
lib/services/user_service.dart
Normal file
@@ -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<File> _getUserFile() async {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
return File('${directory.path}/$_fileName');
|
||||
}
|
||||
|
||||
// Charger tous les utilisateurs
|
||||
Future<List<User>> loadUsers() async {
|
||||
try {
|
||||
final file = await _getUserFile();
|
||||
if (!await file.exists()) return [];
|
||||
|
||||
final contents = await file.readAsString();
|
||||
if (contents.isEmpty) return [];
|
||||
|
||||
final List<dynamic> 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<void> saveUsers(List<User> 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<bool> 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<User?> 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<bool> 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<User?> 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<User?> 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<bool> 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<bool> 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<bool> 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<bool> 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<int> 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<void> 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<void> 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user