<?php
declare(strict_types=1);
namespace App\EventsSubscriber\User;
use App\Entity\User;
use App\Services\Creator\CreatorServiceInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class ApiCheckRouteAccessSubscriber implements EventSubscriberInterface
{
private const PATTERN_USER_IN_URL = '/\/api\/(v1|v2)\/(creators|sponsors)\/([0-9]+)/';
private const PATTERN_PROFILE_IN_URL = '/\/api\/(v1|v2)\/(creators|sponsors)\/(profiles|dashboards)\/([0-9]+)/';
public function __construct(
protected TokenStorageInterface $tokenStorage,
private readonly CreatorServiceInterface $creatorService,
) {
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => [
['checkAccess'],
],
];
}
public function checkAccess(RequestEvent $requestEvent): void
{
if (!$requestEvent->getRequest()->attributes->has('_api_operation_name')) {
return;
}
$routeContext = $this->parseRouteUserFromUri($requestEvent);
if ($routeContext === null) {
return;
}
['segment' => $segment, 'userId' => $userId] = $routeContext;
$authorizedUserId = $this->getAuthorizedUserIdFromRequest($requestEvent);
if ($authorizedUserId === null) {
/** @var User|null $user */
$user = $this->tokenStorage->getToken()?->getUser();
$authorizedUserId = $this->getAuthorizedUserId($user);
}
if ($authorizedUserId === null) {
throw new AccessDeniedHttpException();
}
if ($authorizedUserId === $userId) {
return;
}
if (
$segment === 'creators'
&& $this->creatorService->isLinkedCreatorProfile($authorizedUserId, $userId)
) {
return;
}
throw new AccessDeniedHttpException();
}
/**
* @return array{segment: string, userId: int}|null
*/
private function parseRouteUserFromUri(RequestEvent $requestEvent): ?array
{
$pathOrUri = $requestEvent->getRequest()->getPathInfo() ?: $requestEvent->getRequest()->getUri();
preg_match(self::PATTERN_USER_IN_URL, $pathOrUri, $matches);
if (($matches[2] ?? null) !== null && ($matches[3] ?? null) !== null) {
return ['segment' => $matches[2], 'userId' => (int) $matches[3]];
}
preg_match(self::PATTERN_PROFILE_IN_URL, $pathOrUri, $matches);
if (($matches[2] ?? null) !== null && ($matches[4] ?? null) !== null) {
return ['segment' => $matches[2], 'userId' => (int) $matches[4]];
}
return null;
}
private function getAuthorizedUserId(?User $user): ?int
{
if ($user === null) {
return null;
}
if (method_exists($user, 'getId')) {
$id = $user->getId();
if ($id !== null) {
return (int) $id;
}
}
$identifier = $user->getUserIdentifier();
return is_numeric($identifier) ? (int) $identifier : null;
}
private function getAuthorizedUserIdFromRequest(RequestEvent $requestEvent): ?int
{
$authorization = (string)$requestEvent->getRequest()->headers->get('Authorization', '');
if (!str_starts_with($authorization, 'Bearer ')) {
return null;
}
$token = substr($authorization, 7);
$parts = explode('.', $token);
if (count($parts) < 2) {
return null;
}
$payload = $this->decodeJwtPayload($parts[1]);
$id = $payload['id'] ?? null;
return is_numeric($id) ? (int) $id : null;
}
private function decodeJwtPayload(string $payload): ?array
{
$base64 = strtr($payload, '-_', '+/');
$padded = str_pad($base64, strlen($base64) + (4 - strlen($base64) % 4) % 4, '=', STR_PAD_RIGHT);
$decoded = base64_decode($padded, true);
if ($decoded === false) {
return null;
}
try {
$json = json_decode($decoded, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return null;
}
return is_array($json) ? $json : null;
}
}