apiFeedbackLimiter->create($request->getClientIp() ?? 'unknown'); if (!$limiter->consume()->isAccepted()) { return $this->json(['error' => 'Too many requests'], 429); } if (strlen($request->getContent()) > 4096) { return $this->json(['error' => 'Request body too large'], 413); } $data = json_decode($request->getContent(), true); if (!is_array($data)) { return $this->json(['error' => 'Invalid JSON body'], 400); } // Validation minimale if (!isset($data['lat'], $data['lng'], $data['hasSeaweed'])) { return $this->json(['error' => 'Missing required fields: lat, lng, hasSeaweed'], 422); } $lat = filter_var($data['lat'], FILTER_VALIDATE_FLOAT); $lng = filter_var($data['lng'], FILTER_VALIDATE_FLOAT); if ($lat === false || $lng === false) { return $this->json(['error' => 'lat and lng must be valid floats'], 422); } if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180) { return $this->json(['error' => 'lat/lng out of valid range'], 422); } $feedback = new UserFeedback(); $feedback->setLocation(sprintf('SRID=4326;POINT(%f %f)', $lng, $lat)); $feedback->setHasSeaweed((bool) $data['hasSeaweed']); if (isset($data['density']) && in_array($data['density'], ['low', 'medium', 'high'], true)) { $feedback->setDensity($data['density']); } // Fingerprint anonyme : IP hachée (pas de données personnelles) $ip = $request->getClientIp() ?? 'unknown'; $feedback->setSource(hash('sha256', $ip . date('Y-m-d'))); if (!empty($data['coastalPointId'])) { $spot = $this->em->getRepository(CoastalPoint::class)->find($data['coastalPointId']); if ($spot !== null) { $feedback->setCoastalPoint($spot); } } $this->em->persist($feedback); $this->em->flush(); return $this->json(['id' => (string) $feedback->getId()], 201); } }