From 4969be580913a47586f3c732d501e1371016165d Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Sat, 11 Apr 2026 16:31:56 +0200 Subject: [PATCH] =?UTF-8?q?feat(inscription):=20payload=20enfant/photo=20e?= =?UTF-8?q?t=20limite=20JSON=2015=20Mo=20c=C3=B4t=C3=A9=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Express/Nest : body parser json et urlencoded à 15 Mo pour les photos base64. - Front : envoi d’inscription parent (authService, payload, étape 3). Made-with: Cursor --- backend/src/main.ts | 2 +- .../auth/parent_register_step3_screen.dart | 6 +- frontend/lib/services/api/api_config.dart | 1 - frontend/lib/services/auth_service.dart | 118 +++++++++++++++--- .../utils/parent_registration_payload.dart | 65 +++++++--- 5 files changed, 155 insertions(+), 37 deletions(-) diff --git a/backend/src/main.ts b/backend/src/main.ts index 9d3faf5..bcbaa5a 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -12,7 +12,7 @@ async function bootstrap() { logger: ['error', 'warn', 'log', 'debug', 'verbose'], }); - // Photos inscription (base64) : limite Express par défaut ~100 ko → 413/500 sans ceci + // Inscription (photos base64 dans le JSON) : sans limite > défaut Express (~100 ko) → 413/500 ou corps tronqué. app.useBodyParser('json', { limit: '15mb' }); app.useBodyParser('urlencoded', { extended: true, limit: '15mb' }); diff --git a/frontend/lib/screens/auth/parent_register_step3_screen.dart b/frontend/lib/screens/auth/parent_register_step3_screen.dart index b829db8..e13b3a9 100644 --- a/frontend/lib/screens/auth/parent_register_step3_screen.dart +++ b/frontend/lib/screens/auth/parent_register_step3_screen.dart @@ -149,7 +149,11 @@ class _ParentRegisterStep3ScreenState extends State { final ImagePicker picker = ImagePicker(); try { final XFile? pickedFile = await picker.pickImage( - source: ImageSource.gallery, imageQuality: 70, maxWidth: 1024, maxHeight: 1024); + source: ImageSource.gallery, + imageQuality: 60, + maxWidth: 900, + maxHeight: 900, + ); if (pickedFile != null) { if (childIndex < registrationData.children.length) { final oldChild = registrationData.children[childIndex]; diff --git a/frontend/lib/services/api/api_config.dart b/frontend/lib/services/api/api_config.dart index a219a2d..8c70367 100644 --- a/frontend/lib/services/api/api_config.dart +++ b/frontend/lib/services/api/api_config.dart @@ -1,5 +1,4 @@ class ApiConfig { - // static const String baseUrl = 'http://localhost:3000/api/v1/'; static const String baseUrl = 'https://app.ptits-pas.fr/api/v1'; // Auth endpoints diff --git a/frontend/lib/services/auth_service.dart b/frontend/lib/services/auth_service.dart index ab57adc..ef4c32d 100644 --- a/frontend/lib/services/auth_service.dart +++ b/frontend/lib/services/auth_service.dart @@ -185,7 +185,7 @@ class AuthService { return; } - final decoded = response.body.isNotEmpty ? jsonDecode(response.body) : null; + final decoded = _tryDecodeJsonMap(response.body); final message = _extractErrorMessage(decoded, response.statusCode); throw Exception(message); } @@ -199,37 +199,123 @@ class AuthService { } final body = ParentRegistrationPayload.toJson(data); + final String encodedBody; + try { + encodedBody = jsonEncode(body); + } catch (_) { + throw Exception( + 'Impossible de préparer l’envoi (données trop volumineuses ou invalides). ' + 'Réessayez avec une photo plus légère ou sans caractères inhabituels.', + ); + } - final response = await http.post( - Uri.parse('${ApiConfig.baseUrl}${ApiConfig.registerParent}'), - headers: ApiConfig.headers, - body: jsonEncode(body), - ); + late final http.Response response; + try { + response = await http.post( + Uri.parse('${ApiConfig.baseUrl}${ApiConfig.registerParent}'), + headers: ApiConfig.headers, + body: encodedBody, + ); + } on http.ClientException { + throw Exception( + 'Connexion à ${ApiConfig.baseUrl} impossible. Vérifiez votre réseau, ' + 'un pare-feu ou un bloqueur, puis réessayez.', + ); + } if (response.statusCode == 200 || response.statusCode == 201) { return; } - final decoded = response.body.isNotEmpty ? jsonDecode(response.body) : null; - final message = _extractErrorMessage(decoded, response.statusCode); + final decoded = _tryDecodeJsonMap(response.body); + var message = _extractErrorMessage(decoded, response.statusCode); + message = _humanizeParentRegistrationHttpError(message, response.statusCode); throw Exception(message); } + static Map? _tryDecodeJsonMap(String body) { + if (body.isEmpty) return null; + try { + final decoded = jsonDecode(body); + if (decoded is Map) { + return Map.from(decoded); + } + return null; + } catch (_) { + return null; + } + } + + /// Aplatit `message` (string, liste, ou objet Nest / AllExceptionsFilter). + static String? _flattenApiMessageField(dynamic field) { + if (field == null) return null; + if (field is String) { + final t = field.trim(); + return t.isEmpty ? null : t; + } + if (field is List) { + final parts = field + .map(_flattenApiMessageField) + .whereType() + .where((s) => s.isNotEmpty) + .toList(); + if (parts.isEmpty) return null; + return parts.join('. '); + } + if (field is Map) { + if (field['message'] != null) { + final inner = _flattenApiMessageField(field['message']); + if (inner != null) return inner; + } + final err = field['error']; + if (err is String && err.trim().isNotEmpty) return err.trim(); + } + return null; + } + /// 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) { - final err = decoded['error']; - return (err is String ? err : err?.toString()) ?? '$fallback ($statusCode)'; + if (decoded == null || decoded is! Map) { + return '$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(); + final map = decoded; + final fromMessage = _flattenApiMessageField(map['message']); + if (fromMessage != null) return fromMessage; + final err = map['error']; + if (err is String && err.trim().isNotEmpty) return err.trim(); return '$fallback ($statusCode)'; } + /// Remplace « Internal server error » (souvent contrainte SQL / 500) par un texte utile à l’inscription. + static String _humanizeParentRegistrationHttpError(String msg, int statusCode) { + if (statusCode == 409) return msg; + + final lower = msg.toLowerCase().trim(); + final looksLikeGenericServerError = lower == 'internal server error' || + lower == 'internal server error.' || + lower.contains('internal server error'); + + if (statusCode >= 500 && looksLikeGenericServerError) { + return 'Impossible d\'enregistrer votre demande pour le moment. ' + 'Souvent, cela signifie que cette adresse e-mail est déjà utilisée : ' + 'connectez-vous ou utilisez une autre adresse pour le parent principal ' + '(et pour le co-parent si vous en indiquez un). ' + 'Si le problème continue, réessayez plus tard ou contactez le support.'; + } + + if (statusCode >= 500) { + if (lower.contains('duplicate') || + lower.contains('unique constraint') || + lower.contains('23505') || + lower.contains('already exist')) { + return 'Cette adresse e-mail semble déjà enregistrée. ' + 'Essayez de vous connecter ou modifiez l\'adresse du parent ou du co-parent.'; + } + } + return msg; + } + /// Rafraîchit le profil utilisateur depuis l'API static Future refreshCurrentUser() async { final token = await TokenService.getToken(); diff --git a/frontend/lib/utils/parent_registration_payload.dart b/frontend/lib/utils/parent_registration_payload.dart index 2c2a639..05f8d79 100644 --- a/frontend/lib/utils/parent_registration_payload.dart +++ b/frontend/lib/utils/parent_registration_payload.dart @@ -102,7 +102,7 @@ class ParentRegistrationPayload { final tel = _normalizePhone(p1.phone); final body = { - 'email': p1.email.trim(), + 'email': normalizeEmailText(p1.email), 'prenom': p1.firstName.trim(), 'nom': p1.lastName.trim(), 'telephone': tel, @@ -188,6 +188,51 @@ class ParentRegistrationPayload { return map; } + /// Sous-type `image/…` pour data-URL et extension côté API (`jpeg`, `png`, `gif`, `heic`, …). + static String _imageMimeFromMagicBytes(Uint8List bytes) { + if (bytes.length >= 8) { + if (bytes[0] == 0x89 && + bytes[1] == 0x50 && + bytes[2] == 0x4E && + bytes[3] == 0x47) { + return 'png'; + } + if (bytes[0] == 0xFF && bytes[1] == 0xD8) { + return 'jpeg'; + } + if (bytes.length >= 12 && + bytes[0] == 0x52 && + bytes[1] == 0x49 && + bytes[2] == 0x46 && + bytes[3] == 0x46) { + return 'webp'; + } + if (bytes.length >= 6 && + bytes[0] == 0x47 && + bytes[1] == 0x49 && + bytes[2] == 0x46 && + bytes[3] == 0x38 && + (bytes[4] == 0x37 || bytes[4] == 0x39) && + bytes[5] == 0x61) { + return 'gif'; + } + if (bytes.length >= 12 && + bytes[4] == 0x66 && + bytes[5] == 0x74 && + bytes[6] == 0x79 && + bytes[7] == 0x70) { + final a = bytes[8], b = bytes[9], c = bytes[10], d = bytes[11]; + if ((a == 0x68 && b == 0x65 && c == 0x69 && d == 0x63) || + (a == 0x68 && b == 0x65 && c == 0x69 && d == 0x78) || + (a == 0x6d && b == 0x69 && c == 0x66 && d == 0x31) || + (a == 0x6d && b == 0x73 && c == 0x66 && d == 0x31)) { + return 'heic'; + } + } + } + return 'jpeg'; + } + /// (`dataUrl`, `filename`) ou `null` si pas de fichier lisible. static (String, String)? _childPhotoBase64(ChildData c, int index, String prenom) { Uint8List? bytes = c.imageBytes; @@ -203,23 +248,7 @@ class ParentRegistrationPayload { } if (bytes.isEmpty) return null; final b64 = base64Encode(bytes); - var mime = 'jpeg'; - if (bytes.length >= 8) { - if (bytes[0] == 0x89 && - bytes[1] == 0x50 && - bytes[2] == 0x4E && - bytes[3] == 0x47) { - mime = 'png'; - } else if (bytes[0] == 0xFF && bytes[1] == 0xD8) { - mime = 'jpeg'; - } else if (bytes.length >= 12 && - bytes[0] == 0x52 && - bytes[1] == 0x49 && - bytes[2] == 0x46 && - bytes[3] == 0x46) { - mime = 'webp'; - } - } + final mime = _imageMimeFromMagicBytes(bytes); final safeName = prenom.isNotEmpty ? '${prenom.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '_')}.$mime' : 'enfant_${index + 1}.$mime';