feat(front): mot de passe oublié — écrans et API (#127)

- Routes /forgot-password et /reset-password (distinct de /create-password).
- AuthService : POST forgot-password (réponse neutre côté UX) et reset-password.
- Login : lien « Mot de passe oublié ? » vers la demande de réinitialisation.

Made-with: Cursor
This commit is contained in:
2026-04-23 17:02:59 +02:00
parent e887562a1c
commit e51e625d93
6 changed files with 474 additions and 2 deletions
@@ -0,0 +1,158 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../services/auth_service.dart';
import '../../utils/email_utils.dart';
import '../../widgets/image_button.dart';
/// Demande de lien de réinitialisation (ticket #127).
class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key});
@override
State<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
}
class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
final _formKey = GlobalKey<FormState>();
final _emailCtrl = TextEditingController();
bool _submitting = false;
String? _fieldError;
@override
void dispose() {
_emailCtrl.dispose();
super.dispose();
}
String? _validateEmail(String? value) {
return validateEmail(value, allowEmpty: false);
}
Future<void> _submit() async {
if (_submitting) return;
setState(() => _fieldError = null);
if (!(_formKey.currentState?.validate() ?? false)) return;
setState(() => _submitting = true);
try {
await AuthService.requestForgotPassword(_emailCtrl.text);
if (!mounted) return;
setState(() => _submitting = false);
await showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: Text('Demande enregistrée', style: GoogleFonts.merienda(fontWeight: FontWeight.w700)),
content: Text(
'Si un compte correspond à cette adresse, vous recevrez un e-mail '
'avec un lien pour réinitialiser votre mot de passe.',
style: GoogleFonts.merienda(),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text('OK', style: GoogleFonts.merienda(color: const Color(0xFF2D6A4F))),
),
],
),
);
if (mounted) context.go('/login');
} catch (e) {
if (!mounted) return;
setState(() {
_submitting = false;
_fieldError = e.toString().replaceAll('Exception: ', '');
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/paper2.png'),
fit: BoxFit.cover,
repeat: ImageRepeat.repeat,
),
),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Card(
color: const Color(0xF4FFFFFF),
margin: const EdgeInsets.all(24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Mot de passe oublié',
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w700),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
'Indiquez ladresse e-mail de votre compte. Nous vous enverrons un lien si elle est reconnue.',
style: GoogleFonts.merienda(fontSize: 14, color: Colors.black87),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
TextFormField(
controller: _emailCtrl,
keyboardType: TextInputType.emailAddress,
autocorrect: false,
validator: _validateEmail,
decoration: const InputDecoration(
labelText: 'E-mail',
border: OutlineInputBorder(),
),
),
if (_fieldError != null) ...[
const SizedBox(height: 12),
Text(
_fieldError!,
style: GoogleFonts.merienda(color: Colors.red.shade700, fontSize: 13),
),
],
const SizedBox(height: 20),
_submitting
? const Center(child: CircularProgressIndicator())
: ImageButton(
bg: 'assets/images/bg_green.png',
width: double.infinity,
height: 44,
text: 'Envoyer le lien',
textColor: const Color(0xFF2D6A4F),
onPressed: _submit,
),
const SizedBox(height: 12),
TextButton(
onPressed: () => context.go('/login'),
child: Text(
'Retour à la connexion',
style: GoogleFonts.merienda(
color: const Color(0xFF2D6A4F),
decoration: TextDecoration.underline,
),
),
),
],
),
),
),
),
),
),
),
);
}
}