From 660907cbd16a64907f989eab3e02ac7fdcd0a459 Mon Sep 17 00:00:00 2001 From: "m::r" Date: Tue, 6 Jan 2026 21:16:41 +0000 Subject: [PATCH 1/5] feat(scores): show top 10, cleanup --- app/src/Flags/Repository/UserRepository.php | 48 ++----------------- .../Flags/CorrectFlagEndpointTest.php | 4 +- 2 files changed, 5 insertions(+), 47 deletions(-) diff --git a/app/src/Flags/Repository/UserRepository.php b/app/src/Flags/Repository/UserRepository.php index 0ae23fa..f15ad99 100644 --- a/app/src/Flags/Repository/UserRepository.php +++ b/app/src/Flags/Repository/UserRepository.php @@ -4,7 +4,6 @@ use App\Flags\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\Common\Collections\Criteria; use Doctrine\Persistence\ManagerRegistry; use League\OAuth2\Client\Provider\GenericResourceOwner; use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface; @@ -13,6 +12,7 @@ /** * @method User|null find($id, $lockMode = null, $lockVersion = null) * @method User[] findAll() + * @method User|null findOneBy(array $criteria) * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class UserRepository extends ServiceEntityRepository implements UserLoaderInterface @@ -22,7 +22,7 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, User::class); } - public function getHighScores() + public function getHighScores(): array { return $this->createQueryBuilder('u') ->select('u.firstName') @@ -32,53 +32,11 @@ public function getHighScores() ->addSelect('u.gamesTotal') ->addOrderBy('u.highScore', 'DESC') ->addOrderBy('u.bestTime', 'ASC') - ->setMaxResults(5) - ->getQuery() - ->getScalarResult() - ; - } - - public function getAnyUser(): User - { - return $this->matching( - ($criteria = new Criteria()) - ->where( - $criteria - ->expr() - ->gt('id', 0) - ) - ->setMaxResults(1) - )->get(0); - } - - // /** - // * @return User[] Returns an array of User objects - // */ - /* - public function findByExampleField($value) - { - return $this->createQueryBuilder('u') - ->andWhere('u.exampleField = :val') - ->setParameter('val', $value) - ->orderBy('u.id', 'ASC') ->setMaxResults(10) ->getQuery() - ->getResult() - ; - } - */ - - /* - public function findOneBySomeField($value): ?User - { - return $this->createQueryBuilder('u') - ->andWhere('u.exampleField = :val') - ->setParameter('val', $value) - ->getQuery() - ->getOneOrNullResult() + ->getScalarResult() ; } - */ public function loadUserByIdentifier(string $identifier): ?UserInterface { diff --git a/app/tests/Functional/Flags/CorrectFlagEndpointTest.php b/app/tests/Functional/Flags/CorrectFlagEndpointTest.php index 5739ff8..0ded8f5 100644 --- a/app/tests/Functional/Flags/CorrectFlagEndpointTest.php +++ b/app/tests/Functional/Flags/CorrectFlagEndpointTest.php @@ -7,7 +7,7 @@ use App\Flags\Entity\Flag; use App\Tests\Functional\ApiTestCase; use App\Tests\Support\DataProvider\FlagDataProvider; -use Doctrine\ORM\Exception\ORMException; +use Doctrine\DBAL\Exception; final class CorrectFlagEndpointTest extends ApiTestCase { @@ -57,7 +57,7 @@ public function testCorrectEndpointRequiresAuthentication(): void } /** - * @throws ORMException + * @throws Exception */ public function testCorrectEndpointIncrementsCounter(): void { From b858a946d16e361b0ee7f66506b6b2096bf4e0a3 Mon Sep 17 00:00:00 2001 From: "m::r" Date: Tue, 6 Jan 2026 21:44:11 +0000 Subject: [PATCH 2/5] fix(codestyle) --- app/Makefile | 10 +- .../ConsoleCommand/SetWebhookCommand.php | 18 ++- .../Flags/Controller/CapitalsController.php | 8 +- app/src/Flags/Controller/FlagsController.php | 7 +- app/src/Flags/Repository/AnswerRepository.php | 38 +------ .../Flags/Security/HqAuthAuthenticator.php | 103 +++--------------- app/src/Flags/Security/JwksJwtEncoder.php | 56 ++++++++-- app/src/Flags/Service/CapitalsGameService.php | 26 ++++- 8 files changed, 121 insertions(+), 145 deletions(-) diff --git a/app/Makefile b/app/Makefile index 9aed537..29329e1 100644 --- a/app/Makefile +++ b/app/Makefile @@ -41,9 +41,11 @@ composer: ## Install PHP dependencies cache: ## Clear Symfony cache @docker compose exec php bin/console c:c +test-build: + @$(TEST_COMPOSE) build test: ## Run PHPUnit tests (isolated test containers) @echo "Starting test containers..." - @$(TEST_COMPOSE) up -d --build --wait + @$(TEST_COMPOSE) up -d @echo "Setting up test database..." @$(TEST_COMPOSE) exec php bin/console d:d:c -n --if-not-exists @$(TEST_COMPOSE) exec php bin/console d:m:m -n @@ -55,7 +57,7 @@ test: ## Run PHPUnit tests (isolated test containers) @$(TEST_COMPOSE) down test-up: ## Start test containers (keep running) - @$(TEST_COMPOSE) up -d --build --wait + @$(TEST_COMPOSE) up -d @$(TEST_COMPOSE) exec php bin/console d:d:c -n --if-not-exists @$(TEST_COMPOSE) exec php bin/console d:m:m -n @$(TEST_COMPOSE) exec php bin/console app:populate:users -n @@ -90,7 +92,7 @@ coverage-text: ## Show code coverage in terminal @$(TEST_COMPOSE) up -d --build --wait @$(TEST_COMPOSE) exec php bin/console d:d:c -n --if-not-exists @$(TEST_COMPOSE) exec php bin/console d:m:m -n - @$(TEST_COMPOSE) exec php bin/console app:populate:users -n + @$(TEST_COMPOSE) exec php bin/console app:populate:users -n 1 -f Test -l User @$(TEST_COMPOSE) exec php bin/console app:populate:capitals --purge -n @$(TEST_COMPOSE) exec php bin/console app:populate:flags --purge -n @$(TEST_COMPOSE) exec php php -d pcov.enabled=1 vendor/bin/phpunit --coverage-text @@ -103,7 +105,7 @@ qa: ## Run full quality assurance pipeline CS-FIXER PSALM PHPUNIT @$(TEST_COMPOSE) up -d --build --wait @$(TEST_COMPOSE) exec php bin/console d:d:c -n --if-not-exists @$(TEST_COMPOSE) exec php bin/console d:m:m -n - @$(TEST_COMPOSE) exec php bin/console app:populate:users -n + @$(TEST_COMPOSE) exec php bin/console app:populate:users -n 1 -f Test -l User @$(TEST_COMPOSE) exec php bin/console app:populate:capitals --purge -n @$(TEST_COMPOSE) exec php bin/console app:populate:flags --purge -n @echo "" diff --git a/app/src/Flags/ConsoleCommand/SetWebhookCommand.php b/app/src/Flags/ConsoleCommand/SetWebhookCommand.php index fa23974..1652411 100644 --- a/app/src/Flags/ConsoleCommand/SetWebhookCommand.php +++ b/app/src/Flags/ConsoleCommand/SetWebhookCommand.php @@ -9,6 +9,10 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\HttpFoundation\Request; +use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; class SetWebhookCommand extends Command @@ -41,12 +45,22 @@ protected function configure() ; } + /** + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + * @throws RedirectionExceptionInterface + * @throws ClientExceptionInterface + */ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $arg1 = $input->getArgument('arg1'); - // $r = $this->client->request(Request::METHOD_GET, 'https://api.telegram.org/bot' . $this->botToken . '/setWebhook?url=' . $arg1); - $r = $this->client->request(Request::METHOD_GET, 'https://api.telegram.org/bot' . $this->botToken . '/setWebhook?url=' . $arg1); + // $r = $this->client->request( + //Request::METHOD_GET, 'https://api.telegram.org/bot' . $this->botToken . '/setWebhook?url=' . $arg1); + $r = $this->client->request( + Request::METHOD_GET, + 'https://api.telegram.org/bot' . $this->botToken . '/setWebhook?url=' . $arg1, + ); if ($arg1) { $io->note(sprintf('You passed an argument: %s', $arg1)); } diff --git a/app/src/Flags/Controller/CapitalsController.php b/app/src/Flags/Controller/CapitalsController.php index fddf346..9e08621 100644 --- a/app/src/Flags/Controller/CapitalsController.php +++ b/app/src/Flags/Controller/CapitalsController.php @@ -59,8 +59,12 @@ public function gameOver(Request $request, CapitalsGameService $service): JsonRe } #[Route('/capitals/answer/{game}/{countryCode}/{answer}', name: 'get_question_for_game', methods: ['GET'])] - public function getQuestion(Game $game, string $countryCode, string $answer, CapitalsGameService $service): JsonResponse - { + public function getQuestion( + Game $game, + string $countryCode, + string $answer, + CapitalsGameService $service, + ): JsonResponse { return $this->json($service->giveAnswer($countryCode, base64_decode($answer), $game)); } diff --git a/app/src/Flags/Controller/FlagsController.php b/app/src/Flags/Controller/FlagsController.php index b093364..07127f3 100644 --- a/app/src/Flags/Controller/FlagsController.php +++ b/app/src/Flags/Controller/FlagsController.php @@ -107,8 +107,11 @@ public function getHighScores(UserRepository $repository): Response * @throws \JsonException */ #[Route('/scores', name: 'submit_game_results', methods: ['POST'])] - public function postScore(Request $request, EntityManagerInterface $entityManager, #[CurrentUser] User $user): Response - { + public function postScore( + Request $request, + EntityManagerInterface $entityManager, + #[CurrentUser] User $user, + ): Response { $requestArray = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR); $scoreDTO = new ScoreDTO($requestArray); $score = new Score()->fromDTO($scoreDTO); diff --git a/app/src/Flags/Repository/AnswerRepository.php b/app/src/Flags/Repository/AnswerRepository.php index 0e6513c..add8762 100644 --- a/app/src/Flags/Repository/AnswerRepository.php +++ b/app/src/Flags/Repository/AnswerRepository.php @@ -22,7 +22,8 @@ public function __construct(ManagerRegistry $registry) public function findIncorrectGuesses(string $userId): array { - // SELECT COUNT(answer.flag_code) as incorrect, answer.flag_code FROM answer WHERE answer.user_id = 6 AND answer.correct = 0 + // SELECT COUNT(answer.flag_code) as incorrect, + // answer.flag_code FROM answer WHERE answer.user_id = 6 AND answer.correct = 0 // GROUP BY answer.flag_code ORDER BY incorrect DESC; return $this->createQueryBuilder('a') @@ -41,7 +42,8 @@ public function findIncorrectGuesses(string $userId): array public function findCorrectGuesses(string $userId): array { - // SELECT COUNT(answer.flag_code) as incorrect, answer.flag_code FROM answer WHERE answer.user_id = 6 AND answer.correct = 0 + // SELECT COUNT(answer.flag_code) as incorrect, + // answer.flag_code FROM answer WHERE answer.user_id = 6 AND answer.correct = 0 // GROUP BY answer.flag_code ORDER BY incorrect DESC; return $this->createQueryBuilder('a') @@ -60,7 +62,8 @@ public function findCorrectGuesses(string $userId): array public function findAllGuesses(string $userId): array { - // SELECT COUNT(answer.flag_code) as incorrect, answer.flag_code FROM answer WHERE answer.user_id = 6 AND answer.correct = 0 + // SELECT COUNT(answer.flag_code) as incorrect, + // answer.flag_code FROM answer WHERE answer.user_id = 6 AND answer.correct = 0 // GROUP BY answer.flag_code ORDER BY incorrect DESC; return $this->createQueryBuilder('a') @@ -74,33 +77,4 @@ public function findAllGuesses(string $userId): array ->getArrayResult() ; } - - // /** - // * @return Flag[] Returns an array of Flag objects - // */ - /* - public function findByExampleField($value) - { - return $this->createQueryBuilder('f') - ->andWhere('f.exampleField = :val') - ->setParameter('val', $value) - ->orderBy('f.id', 'ASC') - ->setMaxResults(10) - ->getQuery() - ->getResult() - ; - } - */ - - /* - public function findOneBySomeField($value): ?Flag - { - return $this->createQueryBuilder('f') - ->andWhere('f.exampleField = :val') - ->setParameter('val', $value) - ->getQuery() - ->getOneOrNullResult() - ; - } - */ } diff --git a/app/src/Flags/Security/HqAuthAuthenticator.php b/app/src/Flags/Security/HqAuthAuthenticator.php index fac7b11..f48a948 100644 --- a/app/src/Flags/Security/HqAuthAuthenticator.php +++ b/app/src/Flags/Security/HqAuthAuthenticator.php @@ -1,92 +1,11 @@ attributes->get('_route') === 'oauth_check'; -// } -// -// public function authenticate(Request $request): Passport -// { -// $client = $this->clientRegistry->getClient('flags_app'); -// $accessToken = $this->fetchAccessToken($client); -// -// // Store the access token in the request for later use -// $request->attributes->set('oauth_access_token', $accessToken->getToken()); -// $request->attributes->set('oauth_refresh_token', $accessToken->getRefreshToken()); -// $request->attributes->set('oauth_expires_in', $accessToken->getExpires()); -// -// return new SelfValidatingPassport( -// new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) { -// $userInfo = $client->fetchUserFromToken($accessToken); -// return $this->userRepository->loadOrCreateFromOAuth($userInfo); -// }) -// ); -// } -// -// public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response -// { -// // Get the JWT access token -// $accessToken = $request->attributes->get('oauth_access_token'); -// $refreshToken = $request->attributes->get('oauth_refresh_token'); -// $expiresIn = $request->attributes->get('oauth_expires_in'); -// -// // Return JSON with the tokens for the frontend -// return new JsonResponse([ -// 'success' => true, -// 'access_token' => $accessToken, -// 'refresh_token' => $refreshToken, -// 'expires_in' => $expiresIn, -// 'token_type' => 'Bearer', -// 'user' => [ -// // 'email' => $token->getUser()->getEmail(), -// 'roles' => $token->getUser()->getRoles(), -// ] -// ]); -// } -// -// public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response -// { -// return new JsonResponse([ -// 'success' => false, -// 'error' => $exception->getMessage() -// ], Response::HTTP_UNAUTHORIZED); -// } -// } - namespace App\Flags\Security; use App\Flags\Repository\UserRepository; use KnpU\OAuth2ClientBundle\Client\ClientRegistry; use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator; -use Lcobucci\JWT\Parser; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\RouterInterface; @@ -102,7 +21,6 @@ public function __construct( private ClientRegistry $clientRegistry, private RouterInterface $router, private UserRepository $userRepository, - // private Parser $jwtParser, ) { } @@ -140,7 +58,11 @@ public function authenticate(Request $request): Passport ); } - // public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + // public function onAuthenticationSuccess(R + //equest $request, + // TokenInterface $token, + // string $firewallName + //): ?Response // { // return new JsonResponse([$token->getUser()->getUserIdentifier(), implode($token->getUser()->getRoles())]); // // return new RedirectResponse($this->router->generate('app_dashboard')); @@ -177,11 +99,20 @@ public function onAuthenticationSuccess(Request $request, TokenInterface $token, // ]); } - public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response - { + public function onAuthenticationFailure( + Request $request, + AuthenticationException $exception, + ): ?Response { // DEBUG: Log the actual error error_log('OAuth authentication failed: ' . $exception->getMessage()); - error_log('Previous exception: ' . ($exception->getPrevious() ? $exception->getPrevious()->getMessage() : 'none')); + error_log( + sprintf( + 'Previous exception: %s', + $exception->getPrevious() + ? $exception->getPrevious()->getMessage() + : 'none' + ) + ); // Temporarily return error instead of redirect loop return new JsonResponse([ diff --git a/app/src/Flags/Security/JwksJwtEncoder.php b/app/src/Flags/Security/JwksJwtEncoder.php index b1a6d45..2bf9747 100644 --- a/app/src/Flags/Security/JwksJwtEncoder.php +++ b/app/src/Flags/Security/JwksJwtEncoder.php @@ -7,13 +7,14 @@ use App\Flags\Service\JwksService; use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface; use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException; +use Psr\Cache\InvalidArgumentException; use Psr\Log\LoggerInterface; -class JwksJwtEncoder implements JWTEncoderInterface +readonly class JwksJwtEncoder implements JWTEncoderInterface { public function __construct( - private readonly JwksService $jwksService, - private readonly ?LoggerInterface $logger = null, + private JwksService $jwksService, + private ?LoggerInterface $logger = null, ) { } @@ -22,6 +23,11 @@ public function encode(array $data): string throw new \LogicException('This application does not issue tokens. Use the auth server.'); } + /** + * @throws InvalidArgumentException + * @throws JWTDecodeFailureException + * @throws \JsonException + */ public function decode($token): array { $this->logger?->notice('JwksJwtEncoder: CUSTOM ENCODER ACTIVE - decoding token'); @@ -30,22 +36,31 @@ public function decode($token): array if (!$publicKey) { $this->logger?->error('JwksJwtEncoder: Unable to fetch public key from JWKS endpoint'); - throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Unable to fetch public key from JWKS endpoint'); + throw new JWTDecodeFailureException( + JWTDecodeFailureException::INVALID_TOKEN, + 'Unable to fetch public key from JWKS endpoint', + ); } $this->logger?->debug('JwksJwtEncoder: Public key fetched successfully'); $parts = explode('.', $token); if (3 !== \count($parts)) { - throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token format'); + throw new JWTDecodeFailureException( + JWTDecodeFailureException::INVALID_TOKEN, + 'Invalid token format', + ); } [$headerB64, $payloadB64, $signatureB64] = $parts; // Decode header - $header = json_decode($this->base64UrlDecode($headerB64), true); + $header = json_decode($this->base64UrlDecode($headerB64), true, 512, JSON_THROW_ON_ERROR); if (!$header || ($header['alg'] ?? '') !== 'RS256') { - throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Unsupported algorithm or invalid header'); + throw new JWTDecodeFailureException( + JWTDecodeFailureException::INVALID_TOKEN, + 'Unsupported algorithm or invalid header', + ); } // Verify signature @@ -54,7 +69,10 @@ public function decode($token): array $publicKeyResource = openssl_pkey_get_public($publicKey); if (!$publicKeyResource) { - throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid public key'); + throw new JWTDecodeFailureException( + JWTDecodeFailureException::INVALID_TOKEN, + 'Invalid public key', + ); } $isValid = openssl_verify($dataToVerify, $signature, $publicKeyResource, OPENSSL_ALGO_SHA256); @@ -65,25 +83,39 @@ public function decode($token): array if ($publicKey) { $publicKeyResource = openssl_pkey_get_public($publicKey); if ($publicKeyResource) { - $isValid = openssl_verify($dataToVerify, $signature, $publicKeyResource, OPENSSL_ALGO_SHA256); + $isValid = openssl_verify( + $dataToVerify, + $signature, + $publicKeyResource, + OPENSSL_ALGO_SHA256 + ); } } if (1 !== $isValid) { - throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token signature'); + throw new JWTDecodeFailureException( + JWTDecodeFailureException::INVALID_TOKEN, + 'Invalid token signature', + ); } } // Decode payload $payload = json_decode($this->base64UrlDecode($payloadB64), true); if (!$payload) { - throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid payload'); + throw new JWTDecodeFailureException( + JWTDecodeFailureException::INVALID_TOKEN, + 'Invalid payload', + ); } // Check expiration if (isset($payload['exp']) && $payload['exp'] < time()) { $this->logger?->warning('JwksJwtEncoder: Token has expired', ['exp' => $payload['exp']]); - throw new JWTDecodeFailureException(JWTDecodeFailureException::EXPIRED_TOKEN, 'Token has expired'); + throw new JWTDecodeFailureException( + JWTDecodeFailureException::EXPIRED_TOKEN, + 'Token has expired', + ); } $this->logger?->notice('JwksJwtEncoder: CUSTOM ENCODER - Token verified successfully', [ diff --git a/app/src/Flags/Service/CapitalsGameService.php b/app/src/Flags/Service/CapitalsGameService.php index e425bfa..c9271c6 100644 --- a/app/src/Flags/Service/CapitalsGameService.php +++ b/app/src/Flags/Service/CapitalsGameService.php @@ -44,7 +44,7 @@ public function getQuestion(?Game $game = null): array $countries = $this->repository->findBy(['region' => $region], ['id' => 'ASC']); if (!$countries) { - throw new \Exception('no countries found'); + throw new \RuntimeException('no countries found'); } $totalQuestions = count($countries); @@ -74,10 +74,26 @@ public function getQuestion(?Game $game = null): array $correct = array_pop($options); $options = [ - ['option' => $correct->getName(), 'country' => $correct->getCountry(), 'flag' => $this->isoFlags->getEmojiFlag(strtolower($correct->getCode()))], - ['option' => ($entry = array_pop($options))->getName(), 'country' => $entry->getCountry(), 'flag' => $this->isoFlags->getEmojiFlag(strtolower($entry->getCode()))], - ['option' => ($entry = array_pop($options))->getName(), 'country' => $entry->getCountry(), 'flag' => $this->isoFlags->getEmojiFlag(strtolower($entry->getCode()))], - ['option' => ($entry = array_pop($options))->getName(), 'country' => $entry->getCountry(), 'flag' => $this->isoFlags->getEmojiFlag(strtolower($entry->getCode()))], + [ + 'option' => $correct->getName(), + 'country' => $correct->getCountry(), + 'flag' => $this->isoFlags->getEmojiFlag(strtolower($correct->getCode())), + ], + [ + 'option' => ($entry = array_pop($options))->getName(), + 'country' => $entry->getCountry(), + 'flag' => $this->isoFlags->getEmojiFlag(strtolower($entry->getCode())), + ], + [ + 'option' => ($entry = array_pop($options))->getName(), + 'country' => $entry->getCountry(), + 'flag' => $this->isoFlags->getEmojiFlag(strtolower($entry->getCode())), + ], + [ + 'option' => ($entry = array_pop($options))->getName(), + 'country' => $entry->getCountry(), + 'flag' => $this->isoFlags->getEmojiFlag(strtolower($entry->getCode())), + ], ]; shuffle($options); try { From 1a8fc49b8fce0ec4fa6ed32c14a9b81c0c19b6b1 Mon Sep 17 00:00:00 2001 From: "m::r" Date: Tue, 6 Jan 2026 21:56:48 +0000 Subject: [PATCH 3/5] fix(codestyle) --- .../ConsoleCommand/SetWebhookCommand.php | 2 +- app/src/Flags/Entity/CapitalsStat.php | 5 +-- app/src/Flags/Repository/GameRepository.php | 30 +++------------- .../Flags/Security/HqAuthAuthenticator.php | 4 +-- app/src/Flags/Security/JwksJwtEncoder.php | 35 ++++--------------- app/src/Flags/Service/CapitalsGameService.php | 16 +++++---- 6 files changed, 27 insertions(+), 65 deletions(-) diff --git a/app/src/Flags/ConsoleCommand/SetWebhookCommand.php b/app/src/Flags/ConsoleCommand/SetWebhookCommand.php index 1652411..a827b14 100644 --- a/app/src/Flags/ConsoleCommand/SetWebhookCommand.php +++ b/app/src/Flags/ConsoleCommand/SetWebhookCommand.php @@ -56,7 +56,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = new SymfonyStyle($input, $output); $arg1 = $input->getArgument('arg1'); // $r = $this->client->request( - //Request::METHOD_GET, 'https://api.telegram.org/bot' . $this->botToken . '/setWebhook?url=' . $arg1); + // Request::METHOD_GET, 'https://api.telegram.org/bot' . $this->botToken . '/setWebhook?url=' . $arg1); $r = $this->client->request( Request::METHOD_GET, 'https://api.telegram.org/bot' . $this->botToken . '/setWebhook?url=' . $arg1, diff --git a/app/src/Flags/Entity/CapitalsStat.php b/app/src/Flags/Entity/CapitalsStat.php index 58cfad1..21ce594 100644 --- a/app/src/Flags/Entity/CapitalsStat.php +++ b/app/src/Flags/Entity/CapitalsStat.php @@ -3,9 +3,10 @@ namespace App\Flags\Entity; use App\Flags\Entity\Enum\GameType; +use App\Flags\Repository\CapitalsStatRepository; use Doctrine\ORM\Mapping as ORM; -#[ORM\Entity(repositoryClass: "App\Flags\Repository\CapitalsStatRepository")] +#[ORM\Entity(repositoryClass: CapitalsStatRepository::class)] class CapitalsStat { #[ORM\Id] @@ -19,7 +20,7 @@ class CapitalsStat #[ORM\Column(type: 'integer', length: 255)] protected int $score; - #[ORM\ManyToOne(targetEntity: "App\Flags\Entity\User")] + #[ORM\ManyToOne(targetEntity: User::class)] protected readonly User $user; #[ORM\Column(type: 'string', length: 255)] diff --git a/app/src/Flags/Repository/GameRepository.php b/app/src/Flags/Repository/GameRepository.php index f593b86..63cbd05 100644 --- a/app/src/Flags/Repository/GameRepository.php +++ b/app/src/Flags/Repository/GameRepository.php @@ -4,6 +4,7 @@ use App\Flags\Entity\Game; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\ORM\EntityNotFoundException; use Doctrine\Persistence\ManagerRegistry; /** @@ -19,32 +20,9 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, Game::class); } - // /** - // * @return Game[] Returns an array of Game objects - // */ - /* - public function findByExampleField($value) + public function getById(int $id): Game { - return $this->createQueryBuilder('g') - ->andWhere('g.exampleField = :val') - ->setParameter('val', $value) - ->orderBy('g.id', 'ASC') - ->setMaxResults(10) - ->getQuery() - ->getResult() - ; + return $this->findOneBy(['id' => $id]) + ?? throw EntityNotFoundException::fromClassNameAndIdentifier(className: Game::class, id: [$id]); } - */ - - /* - public function findOneBySomeField($value): ?Game - { - return $this->createQueryBuilder('g') - ->andWhere('g.exampleField = :val') - ->setParameter('val', $value) - ->getQuery() - ->getOneOrNullResult() - ; - } - */ } diff --git a/app/src/Flags/Security/HqAuthAuthenticator.php b/app/src/Flags/Security/HqAuthAuthenticator.php index f48a948..839366d 100644 --- a/app/src/Flags/Security/HqAuthAuthenticator.php +++ b/app/src/Flags/Security/HqAuthAuthenticator.php @@ -59,10 +59,10 @@ public function authenticate(Request $request): Passport } // public function onAuthenticationSuccess(R - //equest $request, + // equest $request, // TokenInterface $token, // string $firewallName - //): ?Response + // ): ?Response // { // return new JsonResponse([$token->getUser()->getUserIdentifier(), implode($token->getUser()->getRoles())]); // // return new RedirectResponse($this->router->generate('app_dashboard')); diff --git a/app/src/Flags/Security/JwksJwtEncoder.php b/app/src/Flags/Security/JwksJwtEncoder.php index 2bf9747..40af9ba 100644 --- a/app/src/Flags/Security/JwksJwtEncoder.php +++ b/app/src/Flags/Security/JwksJwtEncoder.php @@ -36,20 +36,14 @@ public function decode($token): array if (!$publicKey) { $this->logger?->error('JwksJwtEncoder: Unable to fetch public key from JWKS endpoint'); - throw new JWTDecodeFailureException( - JWTDecodeFailureException::INVALID_TOKEN, - 'Unable to fetch public key from JWKS endpoint', - ); + throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Unable to fetch public key from JWKS endpoint'); } $this->logger?->debug('JwksJwtEncoder: Public key fetched successfully'); $parts = explode('.', $token); if (3 !== \count($parts)) { - throw new JWTDecodeFailureException( - JWTDecodeFailureException::INVALID_TOKEN, - 'Invalid token format', - ); + throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token format'); } [$headerB64, $payloadB64, $signatureB64] = $parts; @@ -57,10 +51,7 @@ public function decode($token): array // Decode header $header = json_decode($this->base64UrlDecode($headerB64), true, 512, JSON_THROW_ON_ERROR); if (!$header || ($header['alg'] ?? '') !== 'RS256') { - throw new JWTDecodeFailureException( - JWTDecodeFailureException::INVALID_TOKEN, - 'Unsupported algorithm or invalid header', - ); + throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Unsupported algorithm or invalid header'); } // Verify signature @@ -69,10 +60,7 @@ public function decode($token): array $publicKeyResource = openssl_pkey_get_public($publicKey); if (!$publicKeyResource) { - throw new JWTDecodeFailureException( - JWTDecodeFailureException::INVALID_TOKEN, - 'Invalid public key', - ); + throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid public key'); } $isValid = openssl_verify($dataToVerify, $signature, $publicKeyResource, OPENSSL_ALGO_SHA256); @@ -93,29 +81,20 @@ public function decode($token): array } if (1 !== $isValid) { - throw new JWTDecodeFailureException( - JWTDecodeFailureException::INVALID_TOKEN, - 'Invalid token signature', - ); + throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid token signature'); } } // Decode payload $payload = json_decode($this->base64UrlDecode($payloadB64), true); if (!$payload) { - throw new JWTDecodeFailureException( - JWTDecodeFailureException::INVALID_TOKEN, - 'Invalid payload', - ); + throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid payload'); } // Check expiration if (isset($payload['exp']) && $payload['exp'] < time()) { $this->logger?->warning('JwksJwtEncoder: Token has expired', ['exp' => $payload['exp']]); - throw new JWTDecodeFailureException( - JWTDecodeFailureException::EXPIRED_TOKEN, - 'Token has expired', - ); + throw new JWTDecodeFailureException(JWTDecodeFailureException::EXPIRED_TOKEN, 'Token has expired'); } $this->logger?->notice('JwksJwtEncoder: CUSTOM ENCODER - Token verified successfully', [ diff --git a/app/src/Flags/Service/CapitalsGameService.php b/app/src/Flags/Service/CapitalsGameService.php index c9271c6..7f36163 100644 --- a/app/src/Flags/Service/CapitalsGameService.php +++ b/app/src/Flags/Service/CapitalsGameService.php @@ -8,6 +8,7 @@ use App\Flags\Entity\Game; use App\Flags\Entity\User; use App\Flags\Repository\CapitalRepository; +use App\Flags\Repository\CapitalsStatRepository; use App\Flags\Repository\GameRepository; use Doctrine\ORM\EntityManagerInterface; use Rteeom\FlagsGenerator\FlagsGenerator; @@ -21,6 +22,7 @@ public function __construct( private CapitalRepository $repository, private GameRepository $gameRepository, + private CapitalsStatRepository $capitalsStatRepository, private TokenStorageInterface $tokenStorage, private EntityManagerInterface $entityManager, ) { @@ -125,18 +127,20 @@ public function giveAnswer(string $questionCountryCode, string $answer, Game $ga ]; } + /** + * @throws \JsonException + */ public function handleGameOver(Request $request): array { [ 'sessionTimer' => $sessionTimer, 'score' => $score, 'gameId' => $gameId, - ] = json_decode($request->getContent(), true); + ] = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR); /** @var User $user */ - $user = $this->tokenStorage->getToken()->getUser(); - /** @var Game $game */ - $game = $this->gameRepository->findOneById((int) $gameId); + $user = $this->tokenStorage->getToken()?->getUser(); + $game = $this->gameRepository->getById((int) $gameId); $entity = new CapitalsStat($sessionTimer, $score, $user, $game->getType()); $this->entityManager->persist($entity); @@ -152,14 +156,14 @@ public function getHighScores(string $gameType): array 'userName' => $item['firstName'] . ' ' . $item['lastName'], 'score' => $item['score'], 'sessionTimer' => $item['sessionTimer'], ], - $this->entityManager->getRepository(CapitalsStat::class)->getHighScores($gameType) + $this->capitalsStatRepository->getHighScores($gameType) ); } public function startGame(GameType $gameType): Game { /** @var User $user */ - $user = $this->tokenStorage->getToken()->getUser(); + $user = $this->tokenStorage->getToken()?->getUser(); $this->entityManager->persist($game = new Game($user, $gameType)); $this->entityManager->flush(); From 3712f800087c6f27ca6fdb0702aa77fe3004705d Mon Sep 17 00:00:00 2001 From: "m::r" Date: Tue, 6 Jan 2026 22:18:27 +0000 Subject: [PATCH 4/5] fix(psalm) --- app/Makefile | 3 +++ app/psalm-baseline.xml | 8 -------- app/src/Flags/Repository/GameRepository.php | 1 + app/src/Flags/Security/HqAuthAuthenticator.php | 3 +++ app/src/Flags/Security/JwksJwtEncoder.php | 12 ++++++++++-- app/src/Flags/Service/CapitalsGameService.php | 2 +- 6 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/Makefile b/app/Makefile index 29329e1..6423518 100644 --- a/app/Makefile +++ b/app/Makefile @@ -129,6 +129,9 @@ pipeline: qa ## Alias for qa (run full pipeline like GitHub Actions) psalm: ## Run Psalm static analysis @docker compose exec php vendor/bin/psalm --no-cache +psalm-baseline-update: ## Update baseline file (new errors will not be added) + @docker compose exec php vendor/bin/psalm --no-cache --update-baseline + cs-fix: ## Fix code style (PHP CS Fixer + PHPCS) @echo "Fixing code style with PHP CS Fixer..." @docker compose exec php vendor/bin/php-cs-fixer fix diff --git a/app/psalm-baseline.xml b/app/psalm-baseline.xml index 2a817f0..0f300d4 100644 --- a/app/psalm-baseline.xml +++ b/app/psalm-baseline.xml @@ -94,8 +94,6 @@ - - @@ -129,8 +127,6 @@ - - @@ -180,7 +176,6 @@ - @@ -218,7 +213,6 @@ - @@ -228,8 +222,6 @@ - - diff --git a/app/src/Flags/Repository/GameRepository.php b/app/src/Flags/Repository/GameRepository.php index 63cbd05..bac5d33 100644 --- a/app/src/Flags/Repository/GameRepository.php +++ b/app/src/Flags/Repository/GameRepository.php @@ -20,6 +20,7 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, Game::class); } + /** @psalm-suppress PossiblyUnusedParam */ public function getById(int $id): Game { return $this->findOneBy(['id' => $id]) diff --git a/app/src/Flags/Security/HqAuthAuthenticator.php b/app/src/Flags/Security/HqAuthAuthenticator.php index 839366d..db1f5c8 100644 --- a/app/src/Flags/Security/HqAuthAuthenticator.php +++ b/app/src/Flags/Security/HqAuthAuthenticator.php @@ -5,6 +5,7 @@ use App\Flags\Repository\UserRepository; use KnpU\OAuth2ClientBundle\Client\ClientRegistry; use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator; +use Override; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -68,6 +69,7 @@ public function authenticate(Request $request): Passport // // return new RedirectResponse($this->router->generate('app_dashboard')); // } + #[Override] public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { // dd($request->toArray()); @@ -99,6 +101,7 @@ public function onAuthenticationSuccess(Request $request, TokenInterface $token, // ]); } + #[Override] public function onAuthenticationFailure( Request $request, AuthenticationException $exception, diff --git a/app/src/Flags/Security/JwksJwtEncoder.php b/app/src/Flags/Security/JwksJwtEncoder.php index 40af9ba..18b3861 100644 --- a/app/src/Flags/Security/JwksJwtEncoder.php +++ b/app/src/Flags/Security/JwksJwtEncoder.php @@ -35,7 +35,10 @@ public function decode($token): array $publicKey = $this->jwksService->getPublicKey(); if (!$publicKey) { - $this->logger?->error('JwksJwtEncoder: Unable to fetch public key from JWKS endpoint'); + $this->logger?->error( + 'JwksJwtEncoder: Unable to fetch public key from JWKS endpoint', + ); + throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Unable to fetch public key from JWKS endpoint'); } @@ -86,7 +89,12 @@ public function decode($token): array } // Decode payload - $payload = json_decode($this->base64UrlDecode($payloadB64), true); + $payload = json_decode( + $this->base64UrlDecode($payloadB64), + true, + 512, + JSON_THROW_ON_ERROR, + ); if (!$payload) { throw new JWTDecodeFailureException(JWTDecodeFailureException::INVALID_TOKEN, 'Invalid payload'); } diff --git a/app/src/Flags/Service/CapitalsGameService.php b/app/src/Flags/Service/CapitalsGameService.php index 7f36163..bf51ffe 100644 --- a/app/src/Flags/Service/CapitalsGameService.php +++ b/app/src/Flags/Service/CapitalsGameService.php @@ -152,7 +152,7 @@ public function handleGameOver(Request $request): array public function getHighScores(string $gameType): array { return array_map( - fn (array $item) => [ + static fn (array $item) => [ 'userName' => $item['firstName'] . ' ' . $item['lastName'], 'score' => $item['score'], 'sessionTimer' => $item['sessionTimer'], ], From 41d19f83337ce09f5c04a41da385e00e524d7fe8 Mon Sep 17 00:00:00 2001 From: "m::r" Date: Tue, 6 Jan 2026 22:22:18 +0000 Subject: [PATCH 5/5] fix(codestyle) --- app/src/Flags/Security/HqAuthAuthenticator.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/Flags/Security/HqAuthAuthenticator.php b/app/src/Flags/Security/HqAuthAuthenticator.php index db1f5c8..26565f3 100644 --- a/app/src/Flags/Security/HqAuthAuthenticator.php +++ b/app/src/Flags/Security/HqAuthAuthenticator.php @@ -5,7 +5,6 @@ use App\Flags\Repository\UserRepository; use KnpU\OAuth2ClientBundle\Client\ClientRegistry; use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator; -use Override; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -69,7 +68,7 @@ public function authenticate(Request $request): Passport // // return new RedirectResponse($this->router->generate('app_dashboard')); // } - #[Override] + #[\Override] public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { // dd($request->toArray()); @@ -101,7 +100,7 @@ public function onAuthenticationSuccess(Request $request, TokenInterface $token, // ]); } - #[Override] + #[\Override] public function onAuthenticationFailure( Request $request, AuthenticationException $exception,