import "dart:convert"; import "dart:typed_data"; import "package:http/http.dart" as http; import "auth_service.dart"; /// Erreur levée par le client API (serveur injoignable ou réponse en échec). class ApiException implements Exception { const ApiException(this.message); final String message; @override String toString() => message; } class ApiFile { const ApiFile({ required this.bytes, required this.fileName, required this.contentType, }); final Uint8List bytes; final String fileName; final String contentType; } /// Point d'accès unique au backend : centralise l'URL, l'authentification et /// la gestion des erreurs. class ApiClient { ApiClient._(); static const String _baseUrl = "http://localhost:3000/api"; static Future get(String chemin) async { final uri = Uri.parse("$_baseUrl$chemin"); late final http.Response reponse; try { reponse = await http.get(uri, headers: AuthService.requestHeaders()); } catch (_) { throw const ApiException( "Impossible de joindre le serveur. Vérifiez que le backend est démarré.", ); } if (reponse.statusCode >= 200 && reponse.statusCode < 300) { return jsonDecode(reponse.body); } throw ApiException(_messageErreur(reponse)); } static Future getFile( String chemin, { required String fallbackFileName, }) async { final uri = Uri.parse("$_baseUrl$chemin"); late final http.Response reponse; try { reponse = await http.get(uri, headers: AuthService.requestHeaders()); } catch (_) { throw const ApiException( "Impossible de joindre le serveur. Vérifiez que le backend est démarré.", ); } if (reponse.statusCode < 200 || reponse.statusCode >= 300) { throw ApiException(_messageErreur(reponse)); } return ApiFile( bytes: reponse.bodyBytes, fileName: _fileName(reponse, fallbackFileName), contentType: reponse.headers["content-type"] ?? "application/octet-stream", ); } static Future post(String chemin, Map body) async { final uri = Uri.parse("$_baseUrl$chemin"); late final http.Response reponse; try { reponse = await http.post( uri, headers: { ...AuthService.requestHeaders(), "content-type": "application/json", }, body: jsonEncode(body), ); } catch (_) { throw const ApiException( "Impossible de joindre le serveur. Vérifiez que le backend est démarré.", ); } if (reponse.statusCode >= 200 && reponse.statusCode < 300) { return jsonDecode(reponse.body); } throw ApiException(_messageErreur(reponse)); } static Future patch(String chemin, Map body) async { final uri = Uri.parse("$_baseUrl$chemin"); late final http.Response reponse; try { reponse = await http.patch( uri, headers: { ...AuthService.requestHeaders(), "content-type": "application/json", }, body: jsonEncode(body), ); } catch (_) { throw const ApiException( "Impossible de joindre le serveur. Vérifiez que le backend est démarré.", ); } if (reponse.statusCode >= 200 && reponse.statusCode < 300) { return jsonDecode(reponse.body); } throw ApiException(_messageErreur(reponse)); } static String _messageErreur(http.Response reponse) { try { final corps = jsonDecode(reponse.body); if (corps is Map && corps["error"] is Map) { final message = (corps["error"] as Map)["message"]; if (message is String) { return message; } } } catch (_) { // Corps non-JSON : on retombe sur le code HTTP. } return "Erreur ${reponse.statusCode}"; } static String _fileName(http.Response reponse, String fallback) { final disposition = reponse.headers["content-disposition"]; if (disposition == null) return fallback; final match = RegExp( "filename=\"?([^\";]+)\"?", caseSensitive: false, ).firstMatch(disposition); return match?.group(1) ?? fallback; } }