<?php
//----------------------------------------------------------------------
// src/EventSubscriber/ApiRateLimitSubscriber.php
//----------------------------------------------------------------------
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use App\Entity\APIRest\AccessAPI;
use App\Services\APIRest\Tools\APIResponseTools;
use App\Services\LogTools;
use App\Utils\APIError;
class ApiRateLimitSubscriber implements EventSubscriberInterface
{
public function __construct(
private RateLimiterFactory $apiByIpLimiter,
private RateLimiterFactory $apiByUserLimiter,
private TokenStorageInterface $tokenStorage,
private APIResponseTools $apiResponseTools,
private LogTools $logTools,
) {}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onKernelRequest', -10],
];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest())
{
return;
}
$request = $event->getRequest();
$route = $request->attributes->get('_route', '');
// Only apply to API REST routes
if (!str_starts_with($route, 'icod_api_rest_'))
{
return;
}
// Exclude login and lost password routes
if (in_array($route, ['icod_api_rest_authentication_login', 'icod_api_rest_lost_password']))
{
return;
}
$ip = $request->getClientIp() ?? 'unknown';
// Rate limit by IP
$ipLimiter = $this->apiByIpLimiter->create($ip);
$ipLimit = $ipLimiter->consume(1);
if (!$ipLimit->isAccepted())
{
$token = $this->tokenStorage->getToken();
$accessApi = null;
if ($token !== null && ($tokenUser = $token->getUser()) instanceof AccessAPI)
{
$accessApi = $tokenUser;
}
$maxRequests = method_exists($ipLimit, 'getLimit') ? $ipLimit->getLimit() : '';
$this->logTools->apiRestRateLimitLog([
'scope' => 'IP',
'ip' => $ip,
'accessApi' => $accessApi,
'route' => $route,
'max_requests' => $maxRequests,
]);
$apiError = new APIError(APIError::RATE_LIMIT_EXCEEDED);
$event->setResponse($this->apiResponseTools->tooManyRequestsResponse($apiError));
return;
}
// Rate limit by authenticated user
$token = $this->tokenStorage->getToken();
if ($token !== null && ($user = $token->getUser()) instanceof AccessAPI)
{
$userLimiter = $this->apiByUserLimiter->create('user_' . $user->getId());
$userLimit = $userLimiter->consume(1);
if (!$userLimit->isAccepted())
{
$maxRequests = method_exists($userLimit, 'getLimit') ? $userLimit->getLimit() : '';
$this->logTools->apiRestRateLimitLog([
'scope' => 'USER',
'ip' => $ip,
'accessApi' => $user,
'route' => $route,
'max_requests' => $maxRequests,
]);
$apiError = new APIError(APIError::RATE_LIMIT_EXCEEDED);
$event->setResponse($this->apiResponseTools->tooManyRequestsResponse($apiError));
return;
}
}
}
}