feat: système de signalement bug/idée + modal changelog avec limitations

- AppReport entity + migration (type, message, fingerprint SHA-256, created_at)
- POST /api/report avec rate-limit 5/heure par fingerprint IP
- ReportModal : formulaire type radio (bug/idée) + textarea 1 000 chars max
- ChangelogModal : fonctionnalités en prod, roadmap, 5 limitations honnêtes
- Deux boutons flottants bas-gauche (? info + ✉ signalement)
- Légende remontée pour éviter le chevauchement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gwadaking
2026-04-14 12:52:32 -04:00
parent 83690b94ab
commit fd476f50c2
12 changed files with 643 additions and 7 deletions

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Controller;
use App\Entity\AppReport;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/api/report', name: 'api_report_')]
class ReportController extends AbstractController
{
public function __construct(
private EntityManagerInterface $em,
private Connection $connection,
) {}
/**
* POST /api/report
*
* Body : { "type": "bug"|"idea", "message": "..." }
* Rate limit : 5 reports/heure par fingerprint (hash IP).
*/
#[Route('', name: 'create', methods: ['POST'])]
public function create(Request $request): JsonResponse
{
$body = json_decode($request->getContent(), true);
$type = trim((string) ($body['type'] ?? ''));
$message = trim((string) ($body['message'] ?? ''));
if (!in_array($type, ['bug', 'idea'], true)) {
return $this->json(['error' => 'type must be "bug" or "idea"'], 400);
}
if (strlen($message) < 5) {
return $this->json(['error' => 'message too short (min 5 chars)'], 400);
}
if (strlen($message) > 1000) {
return $this->json(['error' => 'message too long (max 1000 chars)'], 400);
}
$fingerprint = hash('sha256', $request->getClientIp() ?? 'unknown');
// Rate limit : 5 rapports par heure par fingerprint
$count = (int) $this->connection->fetchOne(
"SELECT COUNT(*) FROM app_report
WHERE fingerprint = :fp AND created_at >= NOW() - INTERVAL '1 hour'",
['fp' => $fingerprint]
);
if ($count >= 5) {
return $this->json(['error' => 'Rate limit exceeded. Try again later.'], 429);
}
$report = new AppReport();
$report->setType($type);
$report->setMessage($message);
$report->setFingerprint($fingerprint);
$this->em->persist($report);
$this->em->flush();
return $this->json(['ok' => true], 201);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity]
#[ORM\Table(name: 'app_report')]
#[ORM\Index(columns: ['created_at'], name: 'idx_report_created_at')]
class AppReport
{
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
private ?Uuid $id = null;
/** 'bug' ou 'idea' */
#[ORM\Column(length: 10)]
private string $type;
#[ORM\Column(type: 'text')]
private string $message;
/** Hash SHA-256 de l'IP — jamais l'IP brute */
#[ORM\Column(length: 64, nullable: true)]
private ?string $fingerprint = null;
#[ORM\Column]
private \DateTimeImmutable $createdAt;
public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
}
public function getId(): ?Uuid { return $this->id; }
public function getType(): string { return $this->type; }
public function setType(string $type): static { $this->type = $type; return $this; }
public function getMessage(): string { return $this->message; }
public function setMessage(string $message): static { $this->message = $message; return $this; }
public function getFingerprint(): ?string { return $this->fingerprint; }
public function setFingerprint(?string $fingerprint): static { $this->fingerprint = $fingerprint; return $this; }
public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
}