import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import '../models/user.dart'; import '../models/am_registration_data.dart'; import 'api/api_config.dart'; import 'api/tokenService.dart'; import '../utils/nir_utils.dart'; class AuthService { static const String _currentUserKey = 'current_user'; /// Connexion de l'utilisateur /// Retourne l'utilisateur connecté avec le flag changement_mdp_obligatoire static Future login(String email, String password) async { try { final response = await http.post( Uri.parse('${ApiConfig.baseUrl}${ApiConfig.login}'), headers: ApiConfig.headers, body: jsonEncode({ 'email': email, 'password': password, }), ); if (response.statusCode == 200 || response.statusCode == 201) { final data = jsonDecode(response.body); // API renvoie access_token / refresh_token (snake_case) final accessToken = data['access_token'] as String? ?? data['accessToken'] as String?; final refreshToken = data['refresh_token'] as String? ?? data['refreshToken'] as String?; if (accessToken == null) throw Exception('Token absent dans la réponse serveur'); await TokenService.saveToken(accessToken); await TokenService.saveRefreshToken(refreshToken ?? ''); final user = await _fetchUserProfile(accessToken); // Stocker l'utilisateur en cache await _saveCurrentUser(user); return user; } else { final error = jsonDecode(response.body); throw Exception(error['message'] ?? 'Erreur de connexion'); } } catch (e) { if (e is Exception) rethrow; throw Exception('Erreur réseau: impossible de se connecter au serveur'); } } /// Récupère le profil utilisateur via /auth/me static Future _fetchUserProfile(String token) async { try { final response = await http.get( Uri.parse('${ApiConfig.baseUrl}${ApiConfig.authMe}'), headers: ApiConfig.authHeaders(token), ); if (response.statusCode == 200) { final data = jsonDecode(response.body); return AppUser.fromJson(data); } else { throw Exception('Erreur lors de la récupération du profil'); } } catch (e) { if (e is Exception) rethrow; throw Exception('Erreur réseau: impossible de récupérer le profil'); } } /// Changement de mot de passe obligatoire static Future changePasswordRequired({ required String currentPassword, required String newPassword, }) async { final token = await TokenService.getToken(); if (token == null) { throw Exception('Non authentifié'); } try { final response = await http.post( Uri.parse('${ApiConfig.baseUrl}${ApiConfig.changePasswordRequired}'), headers: ApiConfig.authHeaders(token), body: jsonEncode({ 'mot_de_passe_actuel': currentPassword, 'nouveau_mot_de_passe': newPassword, 'confirmation_mot_de_passe': newPassword, }), ); if (response.statusCode != 200 && response.statusCode != 201) { final error = jsonDecode(response.body); throw Exception(error['message'] ?? 'Erreur lors du changement de mot de passe'); } // Après le changement de MDP, rafraîchir le profil utilisateur final user = await _fetchUserProfile(token); await _saveCurrentUser(user); } catch (e) { if (e is Exception) rethrow; throw Exception('Erreur réseau: impossible de changer le mot de passe'); } } /// Déconnexion de l'utilisateur static Future logout() async { await TokenService.clearAll(); final prefs = await SharedPreferences.getInstance(); await prefs.remove(_currentUserKey); } /// Vérifie si l'utilisateur est connecté static Future isLoggedIn() async { final token = await TokenService.getToken(); return token != null; } /// Récupère l'utilisateur connecté depuis le cache static Future getCurrentUser() async { final prefs = await SharedPreferences.getInstance(); final userJson = prefs.getString(_currentUserKey); if (userJson != null) { return AppUser.fromJson(jsonDecode(userJson)); } return null; } /// Sauvegarde l'utilisateur actuel en cache static Future _saveCurrentUser(AppUser user) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_currentUserKey, jsonEncode(user.toJson())); } /// Inscription AM complète (POST /auth/register/am). /// En cas de succès (201), aucune donnée utilisateur retournée ; rediriger vers login. static Future registerAM(AmRegistrationData data) async { String? photoBase64; if (data.photoPath != null && data.photoPath!.isNotEmpty && !data.photoPath!.startsWith('assets/')) { try { final file = File(data.photoPath!); if (await file.exists()) { final bytes = await file.readAsBytes(); photoBase64 = 'data:image/jpeg;base64,${base64Encode(bytes)}'; } } catch (_) {} } final body = { 'email': data.email, 'prenom': data.firstName, 'nom': data.lastName, 'telephone': data.phone, 'adresse': data.streetAddress.isNotEmpty ? data.streetAddress : null, 'code_postal': data.postalCode.isNotEmpty ? data.postalCode : null, 'ville': data.city.isNotEmpty ? data.city : null, if (photoBase64 != null) 'photo_base64': photoBase64, 'consentement_photo': data.photoConsent, 'date_naissance': data.dateOfBirth != null ? '${data.dateOfBirth!.year}-${data.dateOfBirth!.month.toString().padLeft(2, '0')}-${data.dateOfBirth!.day.toString().padLeft(2, '0')}' : null, 'lieu_naissance_ville': data.birthCity.isNotEmpty ? data.birthCity : null, 'lieu_naissance_pays': data.birthCountry.isNotEmpty ? data.birthCountry : null, 'nir': normalizeNir(data.nir), 'numero_agrement': data.agrementNumber, 'capacite_accueil': data.capacity ?? 1, 'biographie': data.presentationText.isNotEmpty ? data.presentationText : null, 'acceptation_cgu': data.cguAccepted, 'acceptation_privacy': data.cguAccepted, }; final response = await http.post( Uri.parse('${ApiConfig.baseUrl}${ApiConfig.registerAM}'), headers: ApiConfig.headers, body: jsonEncode(body), ); if (response.statusCode == 200 || response.statusCode == 201) { return; } final decoded = response.body.isNotEmpty ? jsonDecode(response.body) : null; final message = _extractErrorMessage(decoded, response.statusCode); throw Exception(message); } /// Extrait le message d'erreur des réponses NestJS (message string, array, ou objet). static String _extractErrorMessage(dynamic decoded, int statusCode) { const fallback = 'Erreur lors de l\'inscription'; if (decoded == null || decoded is! Map) return '$fallback ($statusCode)'; final msg = decoded['message']; if (msg == null) return decoded['error'] as String? ?? '$fallback ($statusCode)'; if (msg is String) return msg; if (msg is List) return msg.map((e) => e.toString()).join('. ').trim(); if (msg is Map && msg['message'] != null) return msg['message'].toString(); return '$fallback ($statusCode)'; } /// Rafraîchit le profil utilisateur depuis l'API static Future refreshCurrentUser() async { final token = await TokenService.getToken(); if (token == null) return null; try { final user = await _fetchUserProfile(token); await _saveCurrentUser(user); return user; } catch (e) { return null; } } }