<?php declare(strict_types=1);
namespace Sq\GraphQL\EventSubscriber;
use Sq\Entity\Schema\ORM as Entity;
use Sq\GraphQL\Exception\SqGraphQLException;
use Sq\GraphQL\Security\RateLimiter\UniversalApiAnonymousThrottling;
use Sq\GraphQL\Security\RateLimiter\UniversalApiUserThrottling;
use Sq\Service\Environment;
use Sq\Service\Log\ExceptionLogger;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class UniversalApiThrottlingSubscriber implements EventSubscriberInterface
{
public function __construct(
protected UniversalApiUserThrottling $userThrottling,
protected UniversalApiAnonymousThrottling $anonymousThrottling,
protected TokenStorageInterface $tokenStorage,
protected Environment $env,
protected ExceptionLogger $exceptionLogger
) {
}
public function onController(ControllerEvent $event): void
{
$request = $event->getRequest();
if (!$this->isProd() || !is_graphql_request($request))
{
return;
}
try
{
$loggedInUser = $this->getLoggedInUser();
if ($loggedInUser === null)
{
$this->anonymousThrottling->ifThrottledThrowError($request->getClientIp());
}
else
{
$this->userThrottling->ifThrottledThrowError((string) $loggedInUser->getId());
}
}
catch (SqGraphQLException $e)
{
$this->exceptionLogger->logGqlException($e, $this->getLoggedInUser());
$waitInSeconds = $e->getExtensions()['wait'] ?? null;
$result = new \GraphQL\Executor\ExecutionResult(null, [
new \GraphQL\Error\Error(SqGraphQLException::httpRateLimited()->getMessage(), null, null, [], null, null, is_int($waitInSeconds) ? ['wait' => $waitInSeconds] : []),
]);
$response = new JsonResponse($result->toArray(), 429, is_int($waitInSeconds) ? ['Retry-After' => $waitInSeconds] : []);
$event->setController(fn (): Response => $response);
}
}
protected function getLoggedInUser(): ?Entity\User
{
$user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null;
return $user instanceof Entity\User ? $user : null;
}
protected function isProd(): bool
{
return $this->env->isProd();
}
/** @inheritDoc */
public static function getSubscribedEvents()
{
return [
KernelEvents::CONTROLLER => ['onController', -64],
];
}
}