From 78033dbc3fbceb8f90b2508bd7f92ff17369c43a Mon Sep 17 00:00:00 2001 From: "m::r" Date: Tue, 13 Jan 2026 02:04:31 +0000 Subject: [PATCH 1/4] chore(dev-env): WIP hacky docker setup + k8s update --- app/Makefile | 5 +- app/config/packages/framework.yaml | 15 +- app/config/packages/knpu_oauth2_client.yaml | 16 +- app/config/services.yaml | 28 ++- app/docker-compose.override.yml | 3 + app/docker-compose.yml | 6 + app/makefile.old | 174 ++++++++++++++++++ .../Flags/Controller/SecurityController.php | 6 +- .../Flags/Security/HqAuthAuthenticator.php | 38 +++- k8s/configmap.yaml | 1 + k8s/redis-deployment.yaml | 61 ++++++ k8s/redis-pvc.yaml | 11 ++ k8s/services.yaml | 12 ++ 13 files changed, 359 insertions(+), 17 deletions(-) create mode 100644 app/makefile.old create mode 100644 k8s/redis-deployment.yaml create mode 100644 k8s/redis-pvc.yaml diff --git a/app/Makefile b/app/Makefile index 6b81397..4264838 100644 --- a/app/Makefile +++ b/app/Makefile @@ -1,6 +1,6 @@ include .env -.PHONY: -- +.PHONY: -- redis-sh CYAN := \033[0;36m RESET := \033[0m @@ -155,6 +155,9 @@ clean: ## Clean generated files sh: ## Access PHP container shell @docker compose exec php sh +redis-sh: ## Access PHP container shell + @docker compose exec redis redis-cli + dumper: ## Start var-dump server @docker compose exec php vendor/bin/var-dump-server diff --git a/app/config/packages/framework.yaml b/app/config/packages/framework.yaml index b679507..f3f224b 100644 --- a/app/config/packages/framework.yaml +++ b/app/config/packages/framework.yaml @@ -6,10 +6,15 @@ framework: # Enables session support. Note that the session will ONLY be started if you read or write from it. # Remove or comment this section to explicitly disable session support. session: - handler_id: null - cookie_secure: auto - cookie_samesite: lax - + name: FLAGS_API_SESS + cookie_path: / + cookie_domain: null # Ensures it defaults to the current host +# handler_id: App\Shared\Session\RedisSessionHandler + handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler +# cookie_samesite: lax + cookie_samesite: null +# cookie_secure: auto + cookie_secure: false # Trust proxy headers (k8s, Caddy, ngrok) trusted_proxies: '127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16' trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-port', 'x-forwarded-proto'] @@ -18,3 +23,5 @@ framework: #fragments: true php_errors: log: true + +# trust_proxy_set_remote_addr: true diff --git a/app/config/packages/knpu_oauth2_client.yaml b/app/config/packages/knpu_oauth2_client.yaml index 395aa76..4767486 100644 --- a/app/config/packages/knpu_oauth2_client.yaml +++ b/app/config/packages/knpu_oauth2_client.yaml @@ -13,6 +13,16 @@ knpu_oauth2_client: # Your OAuth2 server's base URL provider_options: domain: '%env(OAUTH_SERVER_URL)%' - urlAuthorize: '%env(OAUTH_SERVER_URL)%/oauth2/authorize' - urlAccessToken: '%env(OAUTH_SERVER_URL)%/oauth2/token' - urlResourceOwnerDetails: '%env(OAUTH_SERVER_URL)%/oauth2/me' \ No newline at end of file + urlAuthorize: '%env(OAUTH_SERVER_URL)%/oauth2/authorize' # Browser-facing + urlAccessToken: '%env(OAUTH_SERVER_URL)%/oauth2/token' # Back-channel + urlResourceOwnerDetails: '%env(OAUTH_SERVER_URL)%/oauth2/userinfo' + +# provider_options: +# domain: 'http://localhost:8547' + # BROWSER REALITY: The user's browser needs to hit your host port +# urlAuthorize: 'http://localhost:8547/oauth2/authorize' +# urlAuthorize: 'http://openid_caddy/oauth2/authorize' +# DOCKER REALITY: PHP container talks directly to the other Caddy container + # We use port 80 because openid_caddy is listening on 80 inside the network +# urlAccessToken: 'http://openid_caddy/oauth2/token' +# urlResourceOwnerDetails: 'http://openid_caddy/oauth2/me' \ No newline at end of file diff --git a/app/config/services.yaml b/app/config/services.yaml index 11d9b67..f241ae2 100644 --- a/app/config/services.yaml +++ b/app/config/services.yaml @@ -55,4 +55,30 @@ services: App\Flags\Security\JwksJwtEncoder: ~ Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface: - alias: App\Flags\Security\JwksJwtEncoder \ No newline at end of file + alias: App\Flags\Security\JwksJwtEncoder + + + ### REDIS START ### + # 1. Define the Redis connection service + # This represents the physical connection to your Redis container/server + Redis: + class: Redis + calls: + - method: connect + arguments: + - '%env(REDIS_HOST)%' + - '%env(int:REDIS_PORT)%' # 'int:' ensures the port is passed as an integer + + # Uncomment if you added a password to your Redis K8s/Docker setup + # - method: auth + # arguments: + # - '%env(REDIS_PASSWORD)%' + + # 2. Register the Session Handler + # This tells Symfony how to use the Redis service above for sessions + Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler: + arguments: + - '@Redis' + # Optional: Add a prefix to avoid collisions with other apps using the same Redis + - { prefix: 'flags_sess_', ttl: 3600 } + ### REDIS END ### \ No newline at end of file diff --git a/app/docker-compose.override.yml b/app/docker-compose.override.yml index 6bf249b..ffcb70b 100644 --- a/app/docker-compose.override.yml +++ b/app/docker-compose.override.yml @@ -10,6 +10,9 @@ services: db: ports: - "33060:3306" + redis: + ports: + - "6379:6379" caddy: ports: - "8000:80" diff --git a/app/docker-compose.yml b/app/docker-compose.yml index 41136c4..ab8cd51 100644 --- a/app/docker-compose.yml +++ b/app/docker-compose.yml @@ -34,6 +34,12 @@ services: restart: always networks: - backend-flags + redis: + image: "redis:7-alpine" + container_name: "redis-flags-api" + restart: always + networks: + - backend-flags networks: backend-flags: external: true diff --git a/app/makefile.old b/app/makefile.old new file mode 100644 index 0000000..6b81397 --- /dev/null +++ b/app/makefile.old @@ -0,0 +1,174 @@ +include .env + +.PHONY: -- + +CYAN := \033[0;36m +RESET := \033[0m + +# Test environment uses isolated containers +TEST_COMPOSE := docker compose -f docker-compose.test.yml -p flags-test + +# Default target +help: ## Show this help message + @printf "\\nUsage: make $(CYAN)[target]$(RESET)\\n\\n" + @echo 'Targets:' + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " $(CYAN)%-22s$(RESET) %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort -f + +init: build up composer import-db ## Full dev setup +run: up ## Start containers + +build: ## Build all containers + @docker compose build + +up: ## Start all containers + @docker compose up -d + +down: ## Stop all containers + @docker compose down + +rebuild: down build up ## Rebuild and restart containers + +db: ## Create database and run migrations + @docker compose exec php bin/console d:d:c --if-not-exists + @docker compose exec php bin/console d:m:m -n + +import-db: ## Import flags database + @bin/console d:d:i flags.sql + +composer: ## Install PHP dependencies + @docker compose exec php composer install + +cache: ## Clear Symfony cache + @docker compose exec php bin/console c:c + +test-build: ## Build test images + @$(TEST_COMPOSE) build +test: ## Run PHPUnit tests (isolated test containers) + @echo "Starting test containers..." + @$(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 + @$(TEST_COMPOSE) exec php bin/console app:populate:users test_user_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 "Running tests..." + @$(TEST_COMPOSE) exec php vendor/bin/phpunit + @$(TEST_COMPOSE) down + +test-up: ## Start test containers (keep running) + @$(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 test_user_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-down: ## Stop test containers + @$(TEST_COMPOSE) down + +test-sh: ## Shell into test PHP container + @$(TEST_COMPOSE) exec php sh + +coverage: ## Generate code coverage report (HTML) - requires PCOV or Xdebug + @echo "Checking for coverage driver..." + @docker compose exec php php -r "if (!extension_loaded('pcov') && !extension_loaded('xdebug')) { echo 'Error: No coverage driver found. Install PCOV or Xdebug.\n'; echo 'In Docker, add to Dockerfile: RUN install-php-extensions pcov\n'; exit(1); }" + @echo "Starting test environment..." + @$(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:capitals --purge -n + @$(TEST_COMPOSE) exec php bin/console app:populate:flags --purge -n + @echo "Generating coverage report..." + @$(TEST_COMPOSE) exec php php -d pcov.enabled=1 vendor/bin/phpunit --coverage-html coverage/html --coverage-text + @echo "Coverage report: coverage/html/index.html" + @$(TEST_COMPOSE) down + +coverage-text: ## Show code coverage in terminal + @echo "Checking for coverage driver..." + @docker compose exec php php -r "if (!extension_loaded('pcov') && !extension_loaded('xdebug')) { echo '\nError: No coverage driver found.\n'; echo 'Install PCOV: In Docker, add to Dockerfile: RUN install-php-extensions pcov\n'; exit(1); }" + @echo "Starting test environment..." + @$(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 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 + @$(TEST_COMPOSE) down + +qa: ## Run full quality assurance pipeline CS-FIXER PSALM PHPUNIT + @echo "=== Running Quality Assurance Pipeline ===" + @echo "" + @echo "1/5 Starting test environment..." + @$(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 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 "" + @echo "2/5 Checking code style (PHP CS Fixer)..." + @$(TEST_COMPOSE) exec php vendor/bin/php-cs-fixer fix --dry-run --diff + @echo "" + @echo "3/5 Checking PSR-12 compliance (PHPCS)..." + @$(TEST_COMPOSE) exec php vendor/bin/phpcs src/ tests/ --standard=phpcs.xml.dist + @echo "" + @echo "4/5 Running Psalm static analysis..." + @$(TEST_COMPOSE) exec php vendor/bin/psalm + @echo "" + @echo "5/5 Running PHPUnit tests..." + @$(TEST_COMPOSE) exec php vendor/bin/phpunit + @echo "" + @echo "=== Quality Assurance Complete ===" + @$(TEST_COMPOSE) down + +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 + +cs-check: ## Check code style without fixing (PHP CS Fixer only) + @echo "Checking code style with PHP CS Fixer..." + @docker compose exec php vendor/bin/php-cs-fixer check + +phpcs-check: ## Check line length and PSR-12 standards (PHPCS only) + @docker compose exec php vendor/bin/phpcs src/ tests/ --standard=phpcs.xml.dist + +phpcs-fix: ## Auto-fix line length and PSR-12 standards (PHPCS only) + @docker compose exec php vendor/bin/phpcbf src/ tests/ --standard=phpcs.xml.dist + +clean: ## Clean generated files + @rm -rf coverage/ + @rm -rf .phpunit.cache/ + @rm -f .php-cs-fixer.cache + @echo "Cleaned generated files" + +sh: ## Access PHP container shell + @docker compose exec php sh + +dumper: ## Start var-dump server + @docker compose exec php vendor/bin/var-dump-server + +network: ## Create Docker network + @docker network create backend-flags + +--: + @docker compose exec php sh -c "$(filter-out $@,$(MAKECMDGOALS) $(MAKEFLAGS))" + +t: ## Quick test filter (uses test containers): make t -- CorrectFlagEndpointTest + @$(TEST_COMPOSE) exec php vendor/bin/phpunit --filter "$(filter-out $@,$(MAKECMDGOALS) $(MAKEFLAGS))" + +cc: ## Delete all cache folders + rm -rf var/cache + +%: + @ \ No newline at end of file diff --git a/app/src/Flags/Controller/SecurityController.php b/app/src/Flags/Controller/SecurityController.php index a42fffa..cc850f1 100644 --- a/app/src/Flags/Controller/SecurityController.php +++ b/app/src/Flags/Controller/SecurityController.php @@ -12,7 +12,7 @@ class SecurityController extends AbstractController { public function __construct( - private HttpClientInterface $httpClient, + private readonly HttpClientInterface $httpClient, ) { } @@ -41,13 +41,13 @@ public function debugHeaders(Request $request): JsonResponse } #[Route('/oauth/check', name: 'oauth_check')] - public function check() + public function check(): void { // This route is handled by the authenticator } #[Route('/logout', name: 'app_logout')] - public function logout() + public function logout(): void { throw new \LogicException('This should never be reached'); } diff --git a/app/src/Flags/Security/HqAuthAuthenticator.php b/app/src/Flags/Security/HqAuthAuthenticator.php index 4e2c197..3799a80 100644 --- a/app/src/Flags/Security/HqAuthAuthenticator.php +++ b/app/src/Flags/Security/HqAuthAuthenticator.php @@ -18,9 +18,9 @@ class HqAuthAuthenticator extends OAuth2Authenticator { public function __construct( - private ClientRegistry $clientRegistry, - private RouterInterface $router, - private UserRepository $userRepository, + private readonly ClientRegistry $clientRegistry, +// private RouterInterface $router, + private readonly UserRepository $userRepository, ) { } @@ -31,17 +31,45 @@ public function supports(Request $request): ?bool public function authenticate(Request $request): Passport { + +// DEBUG: See what session ID we are using +// dump("Session ID: " . $request->getSession()->getId()); + +// DEBUG: See what state is in the URL vs the Session +// dump("URL State: " . $request->query->get('state')); +// dd("Session State: " . $request->getSession()->get('oauth2state')); + + + // Here a callback from oauth server hits with "code" and "state" url params + // after user has successfully authenticated with oauth and granted access $client = $this->clientRegistry->getClient('flags_app'); - $accessToken = $this->fetchAccessToken($client); + // THIS is a magical moment where all happens in parent (OAuth2Authenticator) + // from global state via traits data is collected (OMG!) + // It pulls the "expected" state from the Symfony Session (which is stored in your Redis) + // The Comparison: If they don't match, it throws the InvalidStateException + $accessToken = $this->fetchAccessToken($client); + // A. The Redis Connection (DSN) Changed + //If your .env change updated REDIS_DSN, your "Trip 1" saved the state in one Redis instance (or database index), but your "Trip 2" is looking for it in another. If Redis is empty or the key is missing, Symfony assumes a CSRF attack and says "Invalid State." + //B. The APP_SECRET Changed + // + //Symfony uses the APP_SECRET (from your backend .env) to sign the session cookie. + // + // If you changed the APP_SECRET, the session cookie in your browser becomes invalid. + // + // When you come back from the OAuth server, Symfony can't decrypt your session cookie, creates a new empty session, and finds no state inside it. + // + //C. stateless: true vs false + // + //Notice that your api firewall is stateless: true, but your oauth firewall is stateless: false. If your React app tries to start the login via an /api/... route instead of the /login route, the session will never be saved, and the state check will always fail. // // Optional: parse & verify JWT locally // $jwt = $accessToken->getToken(); // $token = $this->jwtParser->parse($jwt); // e.g., lcobucci/jwt // if (!$token->verify($signer, $publicKey) || $token->isExpired(new \DateTimeImmutable())) { // throw new AuthenticationException('Invalid or expired token'); // } - // 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()); diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml index d00984e..95e58e9 100644 --- a/k8s/configmap.yaml +++ b/k8s/configmap.yaml @@ -23,3 +23,4 @@ data: PHP_FPM_PORT: "59000" # Trust all private network ranges (Traefik -> Caddy -> PHP) TRUSTED_PROXIES: "127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" + REDIS_URL: "redis://redis:6379" \ No newline at end of file diff --git a/k8s/redis-deployment.yaml b/k8s/redis-deployment.yaml new file mode 100644 index 0000000..afb7f93 --- /dev/null +++ b/k8s/redis-deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: flags-api + labels: + app.kubernetes.io/name: redis + app.kubernetes.io/component: cache + app.kubernetes.io/part-of: flags-quiz +spec: + replicas: 1 + # Strategy "Recreate" is safer for single-node PVCs to avoid + # two pods trying to mount the same volume during a rollout. + strategy: + type: Recreate + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:7-alpine + # 'appendonly yes' ensures every write (OAuth state) is logged to disk + command: ["redis-server", "--appendonly", "yes"] + ports: + - containerPort: 6379 + name: redis + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "200m" + + # Liveness: Restart if Redis hangs + livenessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 10 + periodSeconds: 10 + + # Readiness: Don't send traffic until Redis is ready + readinessProbe: + exec: + command: ["redis-cli", "ping"] + initialDelaySeconds: 5 + periodSeconds: 5 + + volumeMounts: + - name: redis-data + mountPath: /data + + volumes: + - name: redis-data + persistentVolumeClaim: + claimName: redis-pvc \ No newline at end of file diff --git a/k8s/redis-pvc.yaml b/k8s/redis-pvc.yaml new file mode 100644 index 0000000..0622c7a --- /dev/null +++ b/k8s/redis-pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: redis-pvc + namespace: flags-api +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi \ No newline at end of file diff --git a/k8s/services.yaml b/k8s/services.yaml index c6aa107..fd770a4 100644 --- a/k8s/services.yaml +++ b/k8s/services.yaml @@ -55,3 +55,15 @@ spec: name: http selector: app.kubernetes.io/name: caddy +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: flags-api +spec: + ports: + - port: 6379 + targetPort: 6379 + selector: + app: redis \ No newline at end of file From 8dc0a4cee4b94f65c6702f287f43ff6f28e2d8b9 Mon Sep 17 00:00:00 2001 From: "m::r" Date: Wed, 14 Jan 2026 01:19:41 +0000 Subject: [PATCH 2/4] fix(flags): fix correct endpoint fail, update local dev env config --- app/Makefile | 35 +++++++++++++++----- app/docker-compose.test.yml | 2 +- app/src/Flags/Controller/FlagsController.php | 10 ++++++ 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/app/Makefile b/app/Makefile index 4264838..40dcf52 100644 --- a/app/Makefile +++ b/app/Makefile @@ -1,12 +1,14 @@ include .env -.PHONY: -- redis-sh +.PHONY: -- redis-sh fixtures fixtures-dev CYAN := \033[0;36m RESET := \033[0m # Test environment uses isolated containers -TEST_COMPOSE := docker compose -f docker-compose.test.yml -p flags-test +TEST_COMPOSE := docker compose -f docker-compose.test.yml -p flags-api-test +COMPOSE_DEV := docker compose +COMPOSE_PROD_LOCAL := docker compose -f docker-compose.yml -p flags-api-prod # Default target help: ## Show this help message @@ -14,15 +16,23 @@ help: ## Show this help message @echo 'Targets:' @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " $(CYAN)%-22s$(RESET) %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort -f -init: build up composer import-db ## Full dev setup +init: build db up ## Full dev setup run: up ## Start containers build: ## Build all containers @docker compose build - up: ## Start all containers @docker compose up -d +dev: build up + +prod-local: build-prod-local up-prod-local +build-prod-local: ## Start all containers + @docker compose -f docker-compose.yml build +up-prod-local: ## Start all containers + @docker compose -f docker-compose.yml up -d + + down: ## Stop all containers @docker compose down @@ -32,8 +42,18 @@ db: ## Create database and run migrations @docker compose exec php bin/console d:d:c --if-not-exists @docker compose exec php bin/console d:m:m -n -import-db: ## Import flags database - @bin/console d:d:i flags.sql +fixtures-dev: + @$(COMPOSE_DEV) exec php bin/console app:populate:users 1 -f Timo -l Maas + @$(COMPOSE_DEV) exec php bin/console app:populate:capitals --purge -n + @$(COMPOSE_DEV) exec php bin/console app:populate:flags --purge -n + +fixtures: + @$(COMPOSE) exec php bin/console app:populate:users test_user_1 -f FirstName -l LastName + @$(COMPOSE) exec php bin/console app:populate:capitals --purge -n + @$(COMPOSE) exec php bin/console app:populate:flags --purge -n + +#import-db: ## Import flags database + @#bin/console d:d:i flags.sql composer: ## Install PHP dependencies @docker compose exec php composer install @@ -173,5 +193,4 @@ t: ## Quick test filter (uses test containers): make t -- CorrectFlagEndpointTes cc: ## Delete all cache folders rm -rf var/cache -%: - @ \ No newline at end of file +%: @ \ No newline at end of file diff --git a/app/docker-compose.test.yml b/app/docker-compose.test.yml index a2b09b1..571bc14 100644 --- a/app/docker-compose.test.yml +++ b/app/docker-compose.test.yml @@ -17,7 +17,7 @@ services: condition: service_healthy db: build: - context: .. + context: ..host.docker.internal dockerfile: .docker/mysql/Dockerfile environment: MYSQL_ROOT_PASSWORD: root diff --git a/app/src/Flags/Controller/FlagsController.php b/app/src/Flags/Controller/FlagsController.php index 3287986..243be67 100644 --- a/app/src/Flags/Controller/FlagsController.php +++ b/app/src/Flags/Controller/FlagsController.php @@ -24,6 +24,7 @@ use Symfony\Component\Security\Http\Attribute\CurrentUser; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Validator\Validator\ValidatorInterface; +use function PhpCsFixer\Fixer\PhpUnit\configurePostNormalisation; #[Route('/api/flags')] class FlagsController extends AbstractController @@ -184,6 +185,13 @@ public function getRight(#[CurrentUser] $user, AnswerRepository $repository): Re // TODO move this logic to service // OMG rewrite this poop asap foreach ($result as $key => $item) { + + if (!$item['flagCode']) { + $result[$key] = null; + + continue; + } + $shown = (int) $item['times']; $result[$key]['times_shown'] = $shown; $result[$key]['times_guessed'] = 0; @@ -206,6 +214,8 @@ public function getRight(#[CurrentUser] $user, AnswerRepository $repository): Re } } + $result = array_filter($result); + array_multisort( $result, SORT_DESC, From 3eee0d0dfe389dc7b3050751681d49eb834c5fee Mon Sep 17 00:00:00 2001 From: "m::r" Date: Wed, 14 Jan 2026 01:43:55 +0000 Subject: [PATCH 3/4] fix(codestyle) --- app/src/Flags/Controller/FlagsController.php | 2 -- .../Flags/Security/HqAuthAuthenticator.php | 24 +++++++++---------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/app/src/Flags/Controller/FlagsController.php b/app/src/Flags/Controller/FlagsController.php index 243be67..4937acc 100644 --- a/app/src/Flags/Controller/FlagsController.php +++ b/app/src/Flags/Controller/FlagsController.php @@ -24,7 +24,6 @@ use Symfony\Component\Security\Http\Attribute\CurrentUser; use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Validator\Validator\ValidatorInterface; -use function PhpCsFixer\Fixer\PhpUnit\configurePostNormalisation; #[Route('/api/flags')] class FlagsController extends AbstractController @@ -185,7 +184,6 @@ public function getRight(#[CurrentUser] $user, AnswerRepository $repository): Re // TODO move this logic to service // OMG rewrite this poop asap foreach ($result as $key => $item) { - if (!$item['flagCode']) { $result[$key] = null; diff --git a/app/src/Flags/Security/HqAuthAuthenticator.php b/app/src/Flags/Security/HqAuthAuthenticator.php index 3799a80..38a4052 100644 --- a/app/src/Flags/Security/HqAuthAuthenticator.php +++ b/app/src/Flags/Security/HqAuthAuthenticator.php @@ -19,7 +19,7 @@ class HqAuthAuthenticator extends OAuth2Authenticator { public function __construct( private readonly ClientRegistry $clientRegistry, -// private RouterInterface $router, + // private RouterInterface $router, private readonly UserRepository $userRepository, ) { } @@ -31,14 +31,12 @@ public function supports(Request $request): ?bool public function authenticate(Request $request): Passport { + // DEBUG: See what session ID we are using + // dump("Session ID: " . $request->getSession()->getId()); -// DEBUG: See what session ID we are using -// dump("Session ID: " . $request->getSession()->getId()); - -// DEBUG: See what state is in the URL vs the Session -// dump("URL State: " . $request->query->get('state')); -// dd("Session State: " . $request->getSession()->get('oauth2state')); - + // DEBUG: See what state is in the URL vs the Session + // dump("URL State: " . $request->query->get('state')); + // dd("Session State: " . $request->getSession()->get('oauth2state')); // Here a callback from oauth server hits with "code" and "state" url params // after user has successfully authenticated with oauth and granted access @@ -50,18 +48,18 @@ public function authenticate(Request $request): Passport // The Comparison: If they don't match, it throws the InvalidStateException $accessToken = $this->fetchAccessToken($client); // A. The Redis Connection (DSN) Changed - //If your .env change updated REDIS_DSN, your "Trip 1" saved the state in one Redis instance (or database index), but your "Trip 2" is looking for it in another. If Redis is empty or the key is missing, Symfony assumes a CSRF attack and says "Invalid State." - //B. The APP_SECRET Changed + // If your .env change updated REDIS_DSN, your "Trip 1" saved the state in one Redis instance (or database index), but your "Trip 2" is looking for it in another. If Redis is empty or the key is missing, Symfony assumes a CSRF attack and says "Invalid State." + // B. The APP_SECRET Changed // - //Symfony uses the APP_SECRET (from your backend .env) to sign the session cookie. + // Symfony uses the APP_SECRET (from your backend .env) to sign the session cookie. // // If you changed the APP_SECRET, the session cookie in your browser becomes invalid. // // When you come back from the OAuth server, Symfony can't decrypt your session cookie, creates a new empty session, and finds no state inside it. // - //C. stateless: true vs false + // C. stateless: true vs false // - //Notice that your api firewall is stateless: true, but your oauth firewall is stateless: false. If your React app tries to start the login via an /api/... route instead of the /login route, the session will never be saved, and the state check will always fail. + // Notice that your api firewall is stateless: true, but your oauth firewall is stateless: false. If your React app tries to start the login via an /api/... route instead of the /login route, the session will never be saved, and the state check will always fail. // // Optional: parse & verify JWT locally // $jwt = $accessToken->getToken(); // $token = $this->jwtParser->parse($jwt); // e.g., lcobucci/jwt From 3d78f114a12aa4fa563107fc077671578956ed99 Mon Sep 17 00:00:00 2001 From: "m::r" Date: Wed, 14 Jan 2026 02:06:21 +0000 Subject: [PATCH 4/4] fix(test) --- app/.env.prod | 2 ++ app/tests/Unit/Security/HqAuthAuthenticatorTest.php | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/.env.prod b/app/.env.prod index c790d18..cc11aea 100644 --- a/app/.env.prod +++ b/app/.env.prod @@ -37,3 +37,5 @@ JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem JWT_PASSPHRASE=c6be11833f7971fab179b813c964d616 JWT_TOKEN_TTL=3600000 ###< lexik/jwt-authentication-bundle ### +REDIS_HOST=redis +REDIS_PORT=6379 \ No newline at end of file diff --git a/app/tests/Unit/Security/HqAuthAuthenticatorTest.php b/app/tests/Unit/Security/HqAuthAuthenticatorTest.php index 0cef37a..689615e 100644 --- a/app/tests/Unit/Security/HqAuthAuthenticatorTest.php +++ b/app/tests/Unit/Security/HqAuthAuthenticatorTest.php @@ -11,7 +11,6 @@ use League\OAuth2\Client\Provider\GenericResourceOwner; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\RouterInterface; /** * Unit tests for HqAuthAuthenticator @@ -20,19 +19,16 @@ class HqAuthAuthenticatorTest extends TestCase { private ClientRegistry $clientRegistry; - private RouterInterface $router; private UserRepository $userRepository; private HqAuthAuthenticator $authenticator; protected function setUp(): void { $this->clientRegistry = $this->createMock(ClientRegistry::class); - $this->router = $this->createMock(RouterInterface::class); $this->userRepository = $this->createMock(UserRepository::class); $this->authenticator = new HqAuthAuthenticator( $this->clientRegistry, - $this->router, $this->userRepository ); }