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
@@ -46,6 +46,9 @@ class ApiConfig {
static const String changePasswordRequired = '/auth/change-password-required';
static const String verifyCreatePasswordToken = '/auth/verify-token';
static const String createPassword = '/auth/create-password';
/// Ticket #127 — mot de passe oublié (back à livrer en parallèle).
static const String forgotPassword = '/auth/forgot-password';
static const String resetPassword = '/auth/reset-password';
// Users endpoints
static const String users = '/users';
+72
View File
@@ -12,6 +12,7 @@ import '../utils/parent_registration_payload.dart';
import 'api/api_config.dart';
import 'api/tokenService.dart';
import '../utils/nir_utils.dart';
import '../utils/email_utils.dart';
class AuthService {
static const String _currentUserKey = 'current_user';
@@ -186,6 +187,77 @@ class AuthService {
throw Exception(_extractErrorMessage(decoded, response.statusCode));
}
/// Demande de réinitialisation du mot de passe (ticket #127).
/// Réponse **toujours neutre** côté UX si le serveur répond (anti-énumération des comptes).
static Future<void> requestForgotPassword(String email) async {
final cleaned = normalizeEmailText(email);
if (cleaned.isEmpty) {
throw Exception('Veuillez saisir votre adresse e-mail.');
}
late final http.Response response;
try {
response = await http.post(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.forgotPassword}'),
headers: ApiConfig.headers,
body: jsonEncode({'email': cleaned}),
);
} on http.ClientException {
throw Exception(
'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau puis réessayez.',
);
}
if (response.statusCode >= 500) {
final decoded = _tryDecodeJsonMap(response.body);
throw Exception(_extractErrorMessage(decoded, response.statusCode));
}
}
/// Réinitialise le mot de passe via le lien e-mail (ticket #127).
static Future<void> resetPasswordWithToken({
required String token,
required String password,
required String passwordConfirmation,
}) async {
final cleaned = token.trim();
if (cleaned.isEmpty) {
throw Exception('Lien invalide ou expiré.');
}
late final http.Response response;
try {
response = await http.post(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.resetPassword}'),
headers: ApiConfig.headers,
body: jsonEncode({
'token': cleaned,
'password': password,
'password_confirmation': passwordConfirmation,
}),
);
} on http.ClientException {
throw Exception(
'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau puis réessayez.',
);
}
if (response.statusCode == 200 || response.statusCode == 201) {
return;
}
if (response.statusCode == 404) {
throw Exception('Lien invalide ou expiré.');
}
final decoded = _tryDecodeJsonMap(response.body);
final msg = _extractErrorMessage(decoded, response.statusCode);
final lower = msg.toLowerCase();
if (response.statusCode == 400 &&
(lower.contains('token') ||
lower.contains('invalide') ||
lower.contains('expiré') ||
lower.contains('expire'))) {
throw Exception('Lien invalide ou expiré.');
}
throw Exception(msg);
}
/// Déconnexion de l'utilisateur
static Future<void> logout() async {
await TokenService.clearAll();