<?php
namespace App\EventSubscriber;
use App\Service\SimpleRateLimiter;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class RateLimitSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly SimpleRateLimiter $limiter,
private readonly LoggerInterface $logger,
) {
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onKernelRequest', 50],
];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
$path = $request->getPathInfo();
$method = strtoupper($request->getMethod());
$rules = [
['pattern' => '#^/api/identity/requests$#', 'methods' => ['POST'], 'limit' => 5, 'window' => 3600],
['pattern' => '#^/api/identity/requests/\d+/proofs$#', 'methods' => ['POST'], 'limit' => 10, 'window' => 3600],
['pattern' => '#^/api/hyper-contact/consent$#', 'methods' => ['POST'], 'limit' => 30, 'window' => 3600],
['pattern' => '#^/api/hyper-contact/nearby$#', 'methods' => ['GET'], 'limit' => 120, 'window' => 3600],
['pattern' => '#^/api/public-places/nearby$#', 'methods' => ['GET'], 'limit' => 180, 'window' => 3600],
['pattern' => '#^/w1/scoring/contest$#', 'methods' => ['POST'], 'limit' => 20, 'window' => 3600],
];
foreach ($rules as $rule) {
if (!preg_match($rule['pattern'], $path)) {
continue;
}
if (!in_array($method, $rule['methods'], true)) {
continue;
}
$ip = $request->getClientIp() ?? '0.0.0.0';
$key = sprintf('%s:%s:%s', $ip, $method, $path);
$result = $this->limiter->hit($key, (int) $rule['limit'], (int) $rule['window']);
if (!$result['allowed']) {
$retryAfter = max(1, $result['reset_at'] - time());
$response = new JsonResponse([
'error' => 'rate_limited',
'retry_after' => $retryAfter,
], 429);
$response->headers->set('Retry-After', (string) $retryAfter);
$this->logger->warning('rate_limited', [
'path' => $path,
'method' => $method,
'ip' => $ip,
]);
$event->setResponse($response);
return;
}
}
}
}