From 2f283469c22b8164c98f8421b8789e9fb65bf8cc Mon Sep 17 00:00:00 2001 From: oabrivard Date: Thu, 12 Feb 2026 23:19:49 +0100 Subject: [PATCH] Finished Phase 5: Infrastructure (Weeks 8-9) --- .dockerignore | 15 + .github/workflows/deploy-dev.yml | 29 ++ .github/workflows/deploy-prod.yml | 29 ++ .github/workflows/security-scan.yml | 63 ++++ .gitignore | 4 + Makefile | 57 ++- README.md | 43 +++ Taskfile.yml | 13 + backend/services/admin-service/go.mod | 2 +- backend/services/admin-service/go.sum | 10 +- backend/services/game-session-service/go.mod | 2 +- backend/services/game-session-service/go.sum | 10 +- backend/services/gateway-service/go.mod | 2 +- backend/services/gateway-service/go.sum | 10 +- backend/services/leaderboard-service/go.mod | 2 +- backend/services/leaderboard-service/go.sum | 10 +- backend/services/question-bank-service/go.mod | 2 +- backend/services/question-bank-service/go.sum | 10 +- backend/services/user-service/go.mod | 2 +- backend/services/user-service/go.sum | 10 +- backend/shared/go.mod | 2 +- backend/shared/go.sum | 10 +- frontend/apps/web/index.html | 2 +- .../node_modules/.vite/vitest/results.json | 2 +- frontend/apps/web/src/hooks/useAuth.ts | 13 +- frontend/apps/web/src/hooks/useTimer.ts | 17 +- frontend/apps/web/src/routes/Game.test.tsx | 2 +- frontend/apps/web/src/routes/Game.tsx | 28 +- frontend/apps/web/src/routes/Home.tsx | 10 +- frontend/apps/web/src/routes/Leaderboard.tsx | 4 +- frontend/apps/web/src/routes/Profile.tsx | 17 +- frontend/apps/web/src/routes/Results.test.tsx | 2 +- frontend/apps/web/src/routes/Results.tsx | 4 +- .../apps/web/src/services/validation.test.ts | 4 +- frontend/apps/web/src/services/validation.ts | 3 +- frontend/apps/web/src/styles/global.css | 10 +- .../node_modules/.vite/vitest/results.json | 2 +- .../src/components/AttemptIndicator.tsx | 9 +- .../src/components/HintButton.test.tsx | 4 +- .../src/components/HintButton.tsx | 9 +- .../src/components/LeaderboardTable.tsx | 11 +- .../src/components/ResultsCard.test.tsx | 7 +- .../src/components/ResultsCard.tsx | 5 +- .../src/components/ScoreDisplay.tsx | 8 +- .../src/components/ThemeBadge.tsx | 3 +- .../ui-components/src/components/Timer.tsx | 13 +- infrastructure/prod/.env.prod.example | 105 ++++++ infrastructure/prod/docker-compose.yml | 329 ++++++++++++++++++ infrastructure/prod/nginx/conf.d/default.conf | 29 ++ infrastructure/prod/nginx/nginx.conf | 27 ++ infrastructure/prod/prometheus/prometheus.yml | 38 ++ infrastructure/services/admin.Dockerfile | 16 + .../services/game-session.Dockerfile | 16 + infrastructure/services/gateway.Dockerfile | 16 + .../services/leaderboard.Dockerfile | 16 + .../services/question-bank.Dockerfile | 16 + infrastructure/services/user.Dockerfile | 16 + tasks/deploy-dev.yml | 49 +++ tasks/deploy-prod.yml | 43 +++ tasks/security-scan.yml | 75 ++++ 60 files changed, 1230 insertions(+), 87 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/deploy-dev.yml create mode 100644 .github/workflows/deploy-prod.yml create mode 100644 .github/workflows/security-scan.yml create mode 100644 Taskfile.yml create mode 100644 infrastructure/prod/.env.prod.example create mode 100644 infrastructure/prod/docker-compose.yml create mode 100644 infrastructure/prod/nginx/conf.d/default.conf create mode 100644 infrastructure/prod/nginx/nginx.conf create mode 100644 infrastructure/prod/prometheus/prometheus.yml create mode 100644 infrastructure/services/admin.Dockerfile create mode 100644 infrastructure/services/game-session.Dockerfile create mode 100644 infrastructure/services/gateway.Dockerfile create mode 100644 infrastructure/services/leaderboard.Dockerfile create mode 100644 infrastructure/services/question-bank.Dockerfile create mode 100644 infrastructure/services/user.Dockerfile create mode 100644 tasks/deploy-dev.yml create mode 100644 tasks/deploy-prod.yml create mode 100644 tasks/security-scan.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d5f03e1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +.gitignore +.vscode +**/.DS_Store +**/node_modules +**/dist +**/coverage +**/.cache +**/*.log +docs +frontend +infrastructure/dev +infrastructure/prod +.tmp +tmp diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..2a00f82 --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,29 @@ +name: Deploy Dev + +on: + push: + branches: + - develop + +jobs: + deploy-dev: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Task + uses: arduino/setup-task@v2 + + - name: Setup SSH agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Deploy to dev host + env: + DEPLOY_HOST: ${{ secrets.DEV_DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.DEV_DEPLOY_USER }} + DEPLOY_PATH: ${{ secrets.DEV_DEPLOY_PATH }} + DEPLOY_REF: develop + run: task cd:deploy-dev diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 0000000..cd03d2c --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,29 @@ +name: Deploy Prod + +on: + push: + tags: + - 'v*' + +jobs: + deploy-prod: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Task + uses: arduino/setup-task@v2 + + - name: Setup SSH agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Deploy to prod host + env: + DEPLOY_HOST: ${{ secrets.PROD_DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.PROD_DEPLOY_USER }} + DEPLOY_PATH: ${{ secrets.PROD_DEPLOY_PATH }} + DEPLOY_REF: ${{ github.ref_name }} + run: task cd:deploy-prod diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml new file mode 100644 index 0000000..88bbd34 --- /dev/null +++ b/.github/workflows/security-scan.yml @@ -0,0 +1,63 @@ +name: Security Scan + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + security-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: backend/go.work + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Enable Corepack + run: corepack enable + + - name: Install frontend dependencies + working-directory: frontend + run: yarn install --immutable + + - name: Install Task + uses: arduino/setup-task@v2 + + - name: Install golangci-lint and gosec + run: | + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + go install github.com/securego/gosec/v2/cmd/gosec@latest + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + + - name: Install Trivy + run: | + sudo apt-get update + sudo apt-get install -y wget gnupg lsb-release + wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo gpg --dearmor -o /usr/share/keyrings/trivy.gpg + echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/trivy.list + sudo apt-get update + sudo apt-get install -y trivy + + - name: Run Task security pipeline + run: task ci:security-scan + + - name: Upload reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: security-scan-reports + path: | + reports/security + reports/tests + reports/docker diff --git a/.gitignore b/.gitignore index ef333c7..3cf86f3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ Thumbs.db .env .env.* !.env.example +!**/.env.prod.example # Node / frontend frontend/node_modules/ @@ -46,3 +47,6 @@ pnpm-debug.log* # Go workspace caches (optional, safe) **/.cache/ + +# CI/CD artifacts +reports/ diff --git a/Makefile b/Makefile index 03b1e65..2a8bc3a 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,10 @@ COMPOSE_DIR := infrastructure/dev COMPOSE_FILE := $(COMPOSE_DIR)/docker-compose.yml ENV_FILE := $(COMPOSE_DIR)/.env COMPOSE_CMD := $(COMPOSE) -f $(COMPOSE_FILE) --env-file $(ENV_FILE) --project-directory $(COMPOSE_DIR) +COMPOSE_PROD_DIR := infrastructure/prod +COMPOSE_PROD_FILE := $(COMPOSE_PROD_DIR)/docker-compose.yml +ENV_PROD_FILE := $(COMPOSE_PROD_DIR)/.env.prod +COMPOSE_PROD_CMD := $(COMPOSE) -f $(COMPOSE_PROD_FILE) --env-file $(ENV_PROD_FILE) --project-directory $(COMPOSE_PROD_DIR) ifneq ("$(wildcard $(ENV_FILE))","") include $(ENV_FILE) @@ -22,8 +26,18 @@ JAEGER_UI_PORT ?= 16686 ZITADEL_ADMIN_USERNAME ?= admin ZITADEL_ADMIN_PASSWORD ?= AdminPassword123! GRAFANA_ADMIN_USER ?= admin +BACKEND_MODULES := \ + services/admin-service \ + services/game-session-service \ + services/gateway-service \ + services/leaderboard-service \ + services/question-bank-service \ + services/user-service \ + shared .PHONY: help dev dev-full dev-auth dev-gateway stop clean \ + infra-build-images infra-up-prod \ + task-security-scan task-deploy-dev task-deploy-prod \ backend-lint backend-test backend-build \ frontend-dev frontend-lint frontend-test frontend-build \ db-up db-down db-logs db-shell redis-shell \ @@ -40,6 +54,13 @@ help: @echo " make dev-gateway - Start core infrastructure + gateway ingress (NGINX + gateway service)" @echo " make stop - Stop all containers" @echo " make clean - Stop containers and remove volumes" + @echo " make infra-build-images - Build all production service images via compose" + @echo " make infra-up-prod - Run production compose stack locally (simulation)" + @echo "" + @echo "Task Workflows:" + @echo " make task-security-scan - Run Task CI security pipeline" + @echo " make task-deploy-dev - Run Task development deployment pipeline" + @echo " make task-deploy-prod - Run Task production deployment pipeline" @echo "" @echo "Backend:" @echo " make backend-lint - Run Go linters" @@ -109,21 +130,38 @@ clean: @echo "Stopping containers and removing volumes..." @$(COMPOSE_CMD) down -v +infra-build-images: + @echo "Building production service images..." + @$(COMPOSE_PROD_CMD) build + +infra-up-prod: + @echo "Starting production compose stack locally..." + @$(COMPOSE_PROD_CMD) up -d + # ============================================================================= # Backend # ============================================================================= backend-lint: @echo "Running Go linters..." - @cd backend && golangci-lint run ./... + @for module in $(BACKEND_MODULES); do \ + echo "-> $$module"; \ + (cd backend/$$module && golangci-lint run ./...); \ + done backend-test: @echo "Running Go tests..." - @cd backend && go test -v -race -cover ./... + @for module in $(BACKEND_MODULES); do \ + echo "-> $$module"; \ + (cd backend/$$module && go test -v -race -cover ./...); \ + done backend-build: @echo "Building all services..." - @cd backend && go build ./... + @for module in $(BACKEND_MODULES); do \ + echo "-> $$module"; \ + (cd backend/$$module && go build ./...); \ + done # ============================================================================= # Frontend @@ -164,6 +202,19 @@ db-shell: redis-shell: @docker exec -it knowfoolery-redis redis-cli +# ============================================================================= +# Task Workflows +# ============================================================================= + +task-security-scan: + @task ci:security-scan + +task-deploy-dev: + @task cd:deploy-dev + +task-deploy-prod: + @task cd:deploy-prod + # ============================================================================= # Combined Commands # ============================================================================= diff --git a/README.md b/README.md index 61dc833..f0b1299 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,49 @@ Know Foolery is a quiz game inspired by the French game "Déconnaissance" (https - [Security](docs/3_guidelines/security-guidelines.md) - [Observability](docs/3_guidelines/observability-guidelines.md) +## Infrastructure + +### Directory Layout +- `infrastructure/dev`: local development compose stack and configs +- `infrastructure/prod`: production compose stack, nginx, and prometheus config +- `infrastructure/services`: Dockerfiles for each backend service +- `tasks`: Go Task CI/CD workflows +- `.github/workflows`: thin GitHub Actions wrappers that trigger Task workflows + +### Production Compose Usage +1. Copy `infrastructure/prod/.env.prod.example` to `infrastructure/prod/.env.prod` and fill secrets. +2. Validate config: +```bash +docker compose -f infrastructure/prod/docker-compose.yml --env-file infrastructure/prod/.env.prod config +``` +3. Build and start: +```bash +docker compose -f infrastructure/prod/docker-compose.yml --env-file infrastructure/prod/.env.prod up -d --build +``` + +### Task Workflows +- `task ci:security-scan`: lint, tests, docker build validation, gosec, trivy scans +- `task cd:deploy-dev`: deploy to dev host over SSH + compose build/up + smoke checks +- `task cd:deploy-prod`: deploy release tag to prod host over SSH + rollback on failure + +Make wrappers: +- `make task-security-scan` +- `make task-deploy-dev` +- `make task-deploy-prod` +- `make infra-build-images` +- `make infra-up-prod` + +### GitHub Actions Secrets +- Shared: `SSH_PRIVATE_KEY` +- Dev deploy: `DEV_DEPLOY_HOST`, `DEV_DEPLOY_USER`, `DEV_DEPLOY_PATH` +- Prod deploy: `PROD_DEPLOY_HOST`, `PROD_DEPLOY_USER`, `PROD_DEPLOY_PATH` + +### Deployment Host Prerequisites +- Linux host with Docker Engine + Docker Compose v2 +- `git` and `curl` installed +- persistent repository checkout at the configured deploy path +- populated `infrastructure/prod/.env.prod` on target host + ### Future Enhancements #### Planned Features diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..8908c44 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,13 @@ +version: '3' + +includes: + ci: + taskfile: ./tasks/security-scan.yml + cd: + taskfile: ./tasks/deploy-dev.yml + +tasks: + default: + desc: Show available tasks + cmds: + - task --list-all diff --git a/backend/services/admin-service/go.mod b/backend/services/admin-service/go.mod index 29484da..74b5578 100644 --- a/backend/services/admin-service/go.mod +++ b/backend/services/admin-service/go.mod @@ -18,7 +18,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect + github.com/gofiber/utils/v2 v2.0.0-rc.4 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/backend/services/admin-service/go.sum b/backend/services/admin-service/go.sum index 14d9e78..bd9826a 100644 --- a/backend/services/admin-service/go.sum +++ b/backend/services/admin-service/go.sum @@ -14,6 +14,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -22,8 +24,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/fiber/v3 v3.0.0-beta.3 h1:7Q2I+HsIqnIEEDB+9oe7Gadpakh6ZLhXpTYz/L20vrg= github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY= -github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co= -github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY= +github.com/gofiber/utils/v2 v2.0.0-rc.4 h1:CDjwPwtwwj1OTIf6v3iRk+D2wcdjUzwk91Ghu2TMNbE= +github.com/gofiber/utils/v2 v2.0.0-rc.4/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -68,6 +70,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4= +github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -79,6 +83,8 @@ github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8 github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= diff --git a/backend/services/game-session-service/go.mod b/backend/services/game-session-service/go.mod index 8333360..9713193 100644 --- a/backend/services/game-session-service/go.mod +++ b/backend/services/game-session-service/go.mod @@ -24,7 +24,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.25.0 // indirect - github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect + github.com/gofiber/utils/v2 v2.0.0-rc.4 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/backend/services/game-session-service/go.sum b/backend/services/game-session-service/go.sum index 1cc8700..d31d3a5 100644 --- a/backend/services/game-session-service/go.sum +++ b/backend/services/game-session-service/go.sum @@ -24,6 +24,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -42,8 +44,8 @@ github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/fiber/v3 v3.0.0-beta.3 h1:7Q2I+HsIqnIEEDB+9oe7Gadpakh6ZLhXpTYz/L20vrg= github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY= -github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co= -github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY= +github.com/gofiber/utils/v2 v2.0.0-rc.4 h1:CDjwPwtwwj1OTIf6v3iRk+D2wcdjUzwk91Ghu2TMNbE= +github.com/gofiber/utils/v2 v2.0.0-rc.4/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -92,6 +94,8 @@ github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93Ge github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4= +github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -103,6 +107,8 @@ github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8 github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/backend/services/gateway-service/go.mod b/backend/services/gateway-service/go.mod index 0548284..cbaa34d 100644 --- a/backend/services/gateway-service/go.mod +++ b/backend/services/gateway-service/go.mod @@ -21,7 +21,7 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect + github.com/gofiber/utils/v2 v2.0.0-rc.4 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/klauspost/compress v1.17.9 // indirect diff --git a/backend/services/gateway-service/go.sum b/backend/services/gateway-service/go.sum index 5ff07eb..b996cf9 100644 --- a/backend/services/gateway-service/go.sum +++ b/backend/services/gateway-service/go.sum @@ -23,6 +23,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -31,8 +33,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/fiber/v3 v3.0.0-beta.3 h1:7Q2I+HsIqnIEEDB+9oe7Gadpakh6ZLhXpTYz/L20vrg= github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY= -github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co= -github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY= +github.com/gofiber/utils/v2 v2.0.0-rc.4 h1:CDjwPwtwwj1OTIf6v3iRk+D2wcdjUzwk91Ghu2TMNbE= +github.com/gofiber/utils/v2 v2.0.0-rc.4/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -77,6 +79,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4= +github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -85,6 +89,8 @@ github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8 github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/backend/services/leaderboard-service/go.mod b/backend/services/leaderboard-service/go.mod index e472f4f..0c49423 100644 --- a/backend/services/leaderboard-service/go.mod +++ b/backend/services/leaderboard-service/go.mod @@ -24,7 +24,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.25.0 // indirect - github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect + github.com/gofiber/utils/v2 v2.0.0-rc.4 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/backend/services/leaderboard-service/go.sum b/backend/services/leaderboard-service/go.sum index 1cc8700..d31d3a5 100644 --- a/backend/services/leaderboard-service/go.sum +++ b/backend/services/leaderboard-service/go.sum @@ -24,6 +24,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -42,8 +44,8 @@ github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/fiber/v3 v3.0.0-beta.3 h1:7Q2I+HsIqnIEEDB+9oe7Gadpakh6ZLhXpTYz/L20vrg= github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY= -github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co= -github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY= +github.com/gofiber/utils/v2 v2.0.0-rc.4 h1:CDjwPwtwwj1OTIf6v3iRk+D2wcdjUzwk91Ghu2TMNbE= +github.com/gofiber/utils/v2 v2.0.0-rc.4/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -92,6 +94,8 @@ github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93Ge github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4= +github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -103,6 +107,8 @@ github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8 github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/backend/services/question-bank-service/go.mod b/backend/services/question-bank-service/go.mod index 47d4ef1..19ecb7d 100644 --- a/backend/services/question-bank-service/go.mod +++ b/backend/services/question-bank-service/go.mod @@ -25,7 +25,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.25.0 // indirect - github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect + github.com/gofiber/utils/v2 v2.0.0-rc.4 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/backend/services/question-bank-service/go.sum b/backend/services/question-bank-service/go.sum index d9556f0..da095cb 100644 --- a/backend/services/question-bank-service/go.sum +++ b/backend/services/question-bank-service/go.sum @@ -26,6 +26,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -44,8 +46,8 @@ github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/fiber/v3 v3.0.0-beta.3 h1:7Q2I+HsIqnIEEDB+9oe7Gadpakh6ZLhXpTYz/L20vrg= github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY= -github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co= -github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY= +github.com/gofiber/utils/v2 v2.0.0-rc.4 h1:CDjwPwtwwj1OTIf6v3iRk+D2wcdjUzwk91Ghu2TMNbE= +github.com/gofiber/utils/v2 v2.0.0-rc.4/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -94,6 +96,8 @@ github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93Ge github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4= +github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -105,6 +109,8 @@ github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8 github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/backend/services/user-service/go.mod b/backend/services/user-service/go.mod index c940c23..e5958f3 100644 --- a/backend/services/user-service/go.mod +++ b/backend/services/user-service/go.mod @@ -23,7 +23,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.25.0 // indirect - github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect + github.com/gofiber/utils/v2 v2.0.0-rc.4 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/backend/services/user-service/go.sum b/backend/services/user-service/go.sum index 4a51bd3..8959382 100644 --- a/backend/services/user-service/go.sum +++ b/backend/services/user-service/go.sum @@ -14,6 +14,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -32,8 +34,8 @@ github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/fiber/v3 v3.0.0-beta.3 h1:7Q2I+HsIqnIEEDB+9oe7Gadpakh6ZLhXpTYz/L20vrg= github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY= -github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co= -github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY= +github.com/gofiber/utils/v2 v2.0.0-rc.4 h1:CDjwPwtwwj1OTIf6v3iRk+D2wcdjUzwk91Ghu2TMNbE= +github.com/gofiber/utils/v2 v2.0.0-rc.4/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -80,6 +82,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4= +github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -91,6 +95,8 @@ github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8 github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= diff --git a/backend/shared/go.mod b/backend/shared/go.mod index d88306d..593368f 100644 --- a/backend/shared/go.mod +++ b/backend/shared/go.mod @@ -34,7 +34,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect + github.com/gofiber/utils/v2 v2.0.0-rc.4 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect diff --git a/backend/shared/go.sum b/backend/shared/go.sum index 688b25c..1495c84 100644 --- a/backend/shared/go.sum +++ b/backend/shared/go.sum @@ -24,6 +24,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -42,8 +44,8 @@ github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/fiber/v3 v3.0.0-beta.3 h1:7Q2I+HsIqnIEEDB+9oe7Gadpakh6ZLhXpTYz/L20vrg= github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY= -github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co= -github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY= +github.com/gofiber/utils/v2 v2.0.0-rc.4 h1:CDjwPwtwwj1OTIf6v3iRk+D2wcdjUzwk91Ghu2TMNbE= +github.com/gofiber/utils/v2 v2.0.0-rc.4/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -98,6 +100,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4= +github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -109,6 +113,8 @@ github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8 github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/frontend/apps/web/index.html b/frontend/apps/web/index.html index 3bcf91c..e9cfd37 100644 --- a/frontend/apps/web/index.html +++ b/frontend/apps/web/index.html @@ -1,4 +1,4 @@ - + diff --git a/frontend/apps/web/node_modules/.vite/vitest/results.json b/frontend/apps/web/node_modules/.vite/vitest/results.json index 6266107..bf63381 100644 --- a/frontend/apps/web/node_modules/.vite/vitest/results.json +++ b/frontend/apps/web/node_modules/.vite/vitest/results.json @@ -1 +1 @@ -{"version":"1.6.1","results":[[":src/services/validation.test.ts",{"duration":3,"failed":false}],[":src/routes/Results.test.tsx",{"duration":62,"failed":false}],[":src/routes/Leaderboard.test.tsx",{"duration":181,"failed":false}],[":src/routes/Game.test.tsx",{"duration":184,"failed":false}]]} \ No newline at end of file +{"version":"1.6.1","results":[[":src/services/validation.test.ts",{"duration":1,"failed":false}],[":src/routes/Results.test.tsx",{"duration":28,"failed":false}],[":src/routes/Leaderboard.test.tsx",{"duration":86,"failed":false}],[":src/routes/Game.test.tsx",{"duration":87,"failed":false}]]} \ No newline at end of file diff --git a/frontend/apps/web/src/hooks/useAuth.ts b/frontend/apps/web/src/hooks/useAuth.ts index 94c9f42..08d880e 100644 --- a/frontend/apps/web/src/hooks/useAuth.ts +++ b/frontend/apps/web/src/hooks/useAuth.ts @@ -1,28 +1,33 @@ import { createSignal } from 'solid-js' const AUTH_TOKEN_KEY = 'kf.auth.token' +type UseAuthResult = { + isAuthenticated: () => boolean + signInDemo: () => void + signOut: () => void +} function readToken(): string | null { if (typeof window === 'undefined') return null return window.localStorage.getItem(AUTH_TOKEN_KEY) } -export function useAuth() { +export function useAuth(): UseAuthResult { const [token, setToken] = createSignal(readToken()) - const signInDemo = () => { + const signInDemo = (): void => { const next = 'demo-token' window.localStorage.setItem(AUTH_TOKEN_KEY, next) setToken(next) } - const signOut = () => { + const signOut = (): void => { window.localStorage.removeItem(AUTH_TOKEN_KEY) setToken(null) } return { - isAuthenticated: () => token() != null, + isAuthenticated: (): boolean => token() != null, signInDemo, signOut, } diff --git a/frontend/apps/web/src/hooks/useTimer.ts b/frontend/apps/web/src/hooks/useTimer.ts index 9cc9530..0a69058 100644 --- a/frontend/apps/web/src/hooks/useTimer.ts +++ b/frontend/apps/web/src/hooks/useTimer.ts @@ -1,13 +1,20 @@ -import { createSignal, onCleanup } from 'solid-js' +import { createSignal, onCleanup, type Accessor } from 'solid-js' -export function useTimer(durationMs: number) { +type UseTimerResult = { + remainingMs: Accessor + isExpired: Accessor + start: () => void + stop: () => void +} + +export function useTimer(durationMs: number): UseTimerResult { const [remainingMs, setRemainingMs] = createSignal(durationMs) const [isExpired, setIsExpired] = createSignal(false) let t: number | undefined let startedAt: number | undefined - const tick = () => { + const tick = (): void => { if (startedAt == null) return const elapsed = Date.now() - startedAt const remain = Math.max(0, durationMs - elapsed) @@ -15,14 +22,14 @@ export function useTimer(durationMs: number) { if (remain === 0) setIsExpired(true) } - const start = () => { + const start = (): void => { if (t != null) return startedAt = Date.now() tick() t = window.setInterval(tick, 250) } - const stop = () => { + const stop = (): void => { if (t != null) { window.clearInterval(t) t = undefined diff --git a/frontend/apps/web/src/routes/Game.test.tsx b/frontend/apps/web/src/routes/Game.test.tsx index f92d281..fb3cb64 100644 --- a/frontend/apps/web/src/routes/Game.test.tsx +++ b/frontend/apps/web/src/routes/Game.test.tsx @@ -4,7 +4,7 @@ import { describe, expect, it, vi } from 'vitest' import GameRoute from './Game' vi.mock('@solidjs/router', () => ({ - useNavigate: () => vi.fn(), + useNavigate: (): ((path: string) => void) => vi.fn(), })) describe('GameRoute', () => { diff --git a/frontend/apps/web/src/routes/Game.tsx b/frontend/apps/web/src/routes/Game.tsx index 87cdab2..95495c4 100644 --- a/frontend/apps/web/src/routes/Game.tsx +++ b/frontend/apps/web/src/routes/Game.tsx @@ -1,5 +1,5 @@ import { useNavigate } from '@solidjs/router' -import { createEffect, createMemo, createSignal, onMount } from 'solid-js' +import { createEffect, createMemo, createSignal, onMount, type JSX } from 'solid-js' import { AnswerInput, @@ -35,10 +35,13 @@ function normalizeAnswer(answer: string): string { return answer.trim().toLowerCase() } -async function estimateLeaderboardPosition(score: number, durationSec: number): Promise { +async function estimateLeaderboardPosition( + score: number, + durationSec: number +): Promise { try { const rows = await leaderboardClient.top10() - const sorted = [...rows].sort((a, b) => { + const sorted = [...rows].sort((a, b): number => { if (b.score !== a.score) return b.score - a.score const durationA = typeof a.durationSec === 'number' ? a.durationSec : Number.MAX_SAFE_INTEGER const durationB = typeof b.durationSec === 'number' ? b.durationSec : Number.MAX_SAFE_INTEGER @@ -47,7 +50,8 @@ async function estimateLeaderboardPosition(score: number, durationSec: number): let rank = 1 for (const row of sorted) { - const rowDuration = typeof row.durationSec === 'number' ? row.durationSec : Number.MAX_SAFE_INTEGER + const rowDuration = + typeof row.durationSec === 'number' ? row.durationSec : Number.MAX_SAFE_INTEGER if (score > row.score) break if (score === row.score && durationSec < rowDuration) break rank += 1 @@ -59,7 +63,7 @@ async function estimateLeaderboardPosition(score: number, durationSec: number): } } -export default function GameRoute() { +export default function GameRoute(): JSX.Element { const navigate = useNavigate() const [question] = createSignal(QUESTION) @@ -74,12 +78,12 @@ export default function GameRoute() { const startedAt = Date.now() const { remainingMs, isExpired, start, stop } = useTimer(durationMs) - onMount(() => start()) + onMount((): void => start()) - const attemptsLeft = createMemo(() => Math.max(0, 3 - attempts())) - const answerLocked = createMemo(() => finished() || attemptsLeft() === 0 || isExpired()) + const attemptsLeft = createMemo((): number => Math.max(0, 3 - attempts())) + const answerLocked = createMemo((): boolean => finished() || attemptsLeft() === 0 || isExpired()) - const finalize = async (finalScore: number, answered: number, correct: number) => { + const finalize = async (finalScore: number, answered: number, correct: number): Promise => { if (finished()) return setFinished(true) stop() @@ -106,7 +110,7 @@ export default function GameRoute() { navigate('/results') } - createEffect(() => { + createEffect((): void => { if (isExpired() && !finished()) { setMessage('Temps écoulé.') const answered = attempts() > 0 ? 1 : 0 @@ -114,7 +118,7 @@ export default function GameRoute() { } }) - const submit = () => { + const submit = (): void => { if (answerLocked()) return const normalized = normalizeAnswer(answer()) @@ -146,7 +150,7 @@ export default function GameRoute() { setAnswer('') } - const confirmHint = () => { + const confirmHint = (): void => { if (hintUsed() || attempts() > 0 || answerLocked()) return setHintUsed(true) setMessage(`Indice: ${question().hint}`) diff --git a/frontend/apps/web/src/routes/Home.tsx b/frontend/apps/web/src/routes/Home.tsx index 347d1d1..9745cdd 100644 --- a/frontend/apps/web/src/routes/Home.tsx +++ b/frontend/apps/web/src/routes/Home.tsx @@ -1,5 +1,5 @@ import { useNavigate } from '@solidjs/router' -import { createSignal } from 'solid-js' +import { createSignal, type JSX } from 'solid-js' import Box from '@suid/material/Box' import Button from '@suid/material/Button' @@ -15,12 +15,12 @@ function readInputValue(event: Event): string { return (event.target as HTMLInputElement).value } -export default function HomeRoute() { +export default function HomeRoute(): JSX.Element { const navigate = useNavigate() const [playerName, setPlayerName] = createSignal('') const [error, setError] = createSignal(null) - const start = () => { + const start = (): void => { const name = playerName().trim() const validationError = validatePlayerName(name) if (validationError) { @@ -42,9 +42,7 @@ export default function HomeRoute() { Bienvenue - - Entre ton pseudo et lance une partie. - + Entre ton pseudo et lance une partie. leaderboardClient.top10()) return ( diff --git a/frontend/apps/web/src/routes/Profile.tsx b/frontend/apps/web/src/routes/Profile.tsx index 355eb61..66b6377 100644 --- a/frontend/apps/web/src/routes/Profile.tsx +++ b/frontend/apps/web/src/routes/Profile.tsx @@ -1,4 +1,4 @@ -import { For, Show, createMemo, createSignal } from 'solid-js' +import { For, Show, createMemo, createSignal, type JSX } from 'solid-js' import Box from '@suid/material/Box' import Button from '@suid/material/Button' @@ -17,7 +17,7 @@ function readInputValue(event: Event): string { return (event.target as HTMLInputElement).value } -export default function ProfileRoute() { +export default function ProfileRoute(): JSX.Element { const { isAuthenticated, signInDemo, signOut } = useAuth() const [name, setName] = createSignal(localStorage.getItem('kf.playerName') ?? '') const [showHints, setShowHints] = createSignal(true) @@ -32,7 +32,7 @@ export default function ProfileRoute() { return { gamesPlayed, averageScore, bestScore } }) - const save = () => { + const save = (): void => { localStorage.setItem('kf.playerName', name().trim()) } @@ -47,7 +47,9 @@ export default function ProfileRoute() { Profil - Connexion requise pour accéder au profil joueur. + + Connexion requise pour accéder au profil joueur. + @@ -78,13 +80,16 @@ export default function ProfileRoute() { 0} - fallback={Aucune partie enregistrée.} + fallback={ + Aucune partie enregistrée. + } > {(item) => ( - Score {item.finalScore} • Réussite {item.successRate}% • Durée {Math.floor(item.durationSec / 60)}m + Score {item.finalScore} • Réussite {item.successRate}% • Durée{' '} + {Math.floor(item.durationSec / 60)}m diff --git a/frontend/apps/web/src/routes/Results.test.tsx b/frontend/apps/web/src/routes/Results.test.tsx index 6cb9b13..e7714b9 100644 --- a/frontend/apps/web/src/routes/Results.test.tsx +++ b/frontend/apps/web/src/routes/Results.test.tsx @@ -4,7 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import ResultsRoute from './Results' vi.mock('@solidjs/router', () => ({ - useNavigate: () => vi.fn(), + useNavigate: (): ((path: string) => void) => vi.fn(), })) describe('ResultsRoute', () => { diff --git a/frontend/apps/web/src/routes/Results.tsx b/frontend/apps/web/src/routes/Results.tsx index 522b833..f82c5c7 100644 --- a/frontend/apps/web/src/routes/Results.tsx +++ b/frontend/apps/web/src/routes/Results.tsx @@ -1,12 +1,12 @@ import { useNavigate } from '@solidjs/router' -import { createMemo } from 'solid-js' +import { createMemo, type JSX } from 'solid-js' import { ResultsCard } from '@knowfoolery/ui-components' import Box from '@suid/material/Box' import { loadLastResult } from '../services/session' -export default function ResultsRoute() { +export default function ResultsRoute(): JSX.Element { const navigate = useNavigate() const result = createMemo(() => loadLastResult()) diff --git a/frontend/apps/web/src/services/validation.test.ts b/frontend/apps/web/src/services/validation.test.ts index 3d3749a..e00c9ad 100644 --- a/frontend/apps/web/src/services/validation.test.ts +++ b/frontend/apps/web/src/services/validation.test.ts @@ -12,6 +12,8 @@ describe('validatePlayerName', () => { }) it('rejects non alphanumeric characters', () => { - expect(validatePlayerName('Player@42')).toBe('Le nom doit contenir seulement lettres, chiffres et espaces') + expect(validatePlayerName('Player@42')).toBe( + 'Le nom doit contenir seulement lettres, chiffres et espaces' + ) }) }) diff --git a/frontend/apps/web/src/services/validation.ts b/frontend/apps/web/src/services/validation.ts index 887624a..3b78e9e 100644 --- a/frontend/apps/web/src/services/validation.ts +++ b/frontend/apps/web/src/services/validation.ts @@ -4,6 +4,7 @@ export function validatePlayerName(value: string): string | null { const name = value.trim() if (name.length < 2) return 'Le nom doit faire au moins 2 caractères' if (name.length > 50) return 'Le nom doit faire au plus 50 caractères' - if (!PLAYER_NAME_REGEX.test(name)) return 'Le nom doit contenir seulement lettres, chiffres et espaces' + if (!PLAYER_NAME_REGEX.test(name)) + return 'Le nom doit contenir seulement lettres, chiffres et espaces' return null } diff --git a/frontend/apps/web/src/styles/global.css b/frontend/apps/web/src/styles/global.css index 92e5758..0218d70 100644 --- a/frontend/apps/web/src/styles/global.css +++ b/frontend/apps/web/src/styles/global.css @@ -1,6 +1,14 @@ :root { color-scheme: dark; - font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, 'Apple Color Emoji', + font-family: + ui-sans-serif, + system-ui, + -apple-system, + Segoe UI, + Roboto, + Helvetica, + Arial, + 'Apple Color Emoji', 'Segoe UI Emoji'; } diff --git a/frontend/shared/ui-components/node_modules/.vite/vitest/results.json b/frontend/shared/ui-components/node_modules/.vite/vitest/results.json index 929932b..f5df778 100644 --- a/frontend/shared/ui-components/node_modules/.vite/vitest/results.json +++ b/frontend/shared/ui-components/node_modules/.vite/vitest/results.json @@ -1 +1 @@ -{"version":"1.6.1","results":[[":src/utils/timer.test.ts",{"duration":7,"failed":false}],[":src/components/AttemptIndicator.test.tsx",{"duration":24,"failed":false}],[":src/components/ResultsCard.test.tsx",{"duration":96,"failed":false}],[":src/components/LeaderboardTable.test.tsx",{"duration":38,"failed":false}],[":src/components/AnswerInput.test.tsx",{"duration":58,"failed":false}],[":src/components/HintButton.test.tsx",{"duration":115,"failed":false}]]} \ No newline at end of file +{"version":"1.6.1","results":[[":src/utils/timer.test.ts",{"duration":1,"failed":false}],[":src/components/AttemptIndicator.test.tsx",{"duration":19,"failed":false}],[":src/components/ResultsCard.test.tsx",{"duration":66,"failed":false}],[":src/components/LeaderboardTable.test.tsx",{"duration":28,"failed":false}],[":src/components/AnswerInput.test.tsx",{"duration":36,"failed":false}],[":src/components/HintButton.test.tsx",{"duration":68,"failed":false}]]} \ No newline at end of file diff --git a/frontend/shared/ui-components/src/components/AttemptIndicator.tsx b/frontend/shared/ui-components/src/components/AttemptIndicator.tsx index db1f727..ae682b3 100644 --- a/frontend/shared/ui-components/src/components/AttemptIndicator.tsx +++ b/frontend/shared/ui-components/src/components/AttemptIndicator.tsx @@ -12,10 +12,11 @@ export type AttemptIndicatorProps = { } const AttemptIndicator: Component = (props) => { - const attemptsMax = () => Math.max(1, props.attemptsMax ?? 3) - const used = () => Math.max(0, Math.min(props.attemptsUsed, attemptsMax())) - const remaining = () => Math.max(0, attemptsMax() - used()) - const formatRemaining = () => props.remainingFormatter?.(remaining()) ?? `${remaining()} restant(s)` + const attemptsMax = (): number => Math.max(1, props.attemptsMax ?? 3) + const used = (): number => Math.max(0, Math.min(props.attemptsUsed, attemptsMax())) + const remaining = (): number => Math.max(0, attemptsMax() - used()) + const formatRemaining = (): string => + props.remainingFormatter?.(remaining()) ?? `${remaining()} restant(s)` return ( diff --git a/frontend/shared/ui-components/src/components/HintButton.test.tsx b/frontend/shared/ui-components/src/components/HintButton.test.tsx index a834b22..0a2d9c9 100644 --- a/frontend/shared/ui-components/src/components/HintButton.test.tsx +++ b/frontend/shared/ui-components/src/components/HintButton.test.tsx @@ -16,7 +16,9 @@ describe('HintButton', () => { await fireEvent.click(screen.getByRole('button', { name: 'Indice (score réduit)' })) expect(screen.getByText("Confirmer l'indice")).toBeTruthy() - await fireEvent.click(screen.getByRole('button', { name: 'Oui, utiliser un indice', hidden: true })) + await fireEvent.click( + screen.getByRole('button', { name: 'Oui, utiliser un indice', hidden: true }) + ) expect(onConfirm).toHaveBeenCalledTimes(1) }) }) diff --git a/frontend/shared/ui-components/src/components/HintButton.tsx b/frontend/shared/ui-components/src/components/HintButton.tsx index ffb22cf..0794a70 100644 --- a/frontend/shared/ui-components/src/components/HintButton.tsx +++ b/frontend/shared/ui-components/src/components/HintButton.tsx @@ -21,7 +21,7 @@ export type HintButtonProps = { const HintButton: Component = (props) => { const [dialogOpen, setDialogOpen] = createSignal(false) - const handleClick = () => { + const handleClick = (): void => { if (props.disabled) return if (props.requiresConfirmation === false) { @@ -32,7 +32,7 @@ const HintButton: Component = (props) => { setDialogOpen(true) } - const confirm = () => { + const confirm = (): void => { setDialogOpen(false) props.onConfirm() } @@ -44,10 +44,11 @@ const HintButton: Component = (props) => { setDialogOpen(false)}> - {props.confirmTitle ?? 'Confirmer l\'indice'} + {props.confirmTitle ?? "Confirmer l'indice"} - {props.confirmMessage ?? 'Demander un indice réduit le score maximal de la question de 2 à 1 point. Continuer ?'} + {props.confirmMessage ?? + 'Demander un indice réduit le score maximal de la question de 2 à 1 point. Continuer ?'} diff --git a/frontend/shared/ui-components/src/components/LeaderboardTable.tsx b/frontend/shared/ui-components/src/components/LeaderboardTable.tsx index e3adc16..d7c7bc6 100644 --- a/frontend/shared/ui-components/src/components/LeaderboardTable.tsx +++ b/frontend/shared/ui-components/src/components/LeaderboardTable.tsx @@ -24,11 +24,18 @@ export type LeaderboardTableProps = { } const LeaderboardTable: Component = (props) => { - const maxRows = () => Math.max(1, props.maxRows ?? 10) + const maxRows = (): number => Math.max(1, props.maxRows ?? 10) return ( }> - 0} fallback={{props.emptyMessage ?? 'Aucun score pour le moment.'}}> + 0} + fallback={ + + {props.emptyMessage ?? 'Aucun score pour le moment.'} + + } + > diff --git a/frontend/shared/ui-components/src/components/ResultsCard.test.tsx b/frontend/shared/ui-components/src/components/ResultsCard.test.tsx index fe46202..57ac9ee 100644 --- a/frontend/shared/ui-components/src/components/ResultsCard.test.tsx +++ b/frontend/shared/ui-components/src/components/ResultsCard.test.tsx @@ -30,7 +30,12 @@ describe('ResultsCard', () => { it('renders fallback when result is null', () => { render(() => ( - + )) expect(screen.getByText('Aucune partie terminée pour le moment.')).toBeTruthy() diff --git a/frontend/shared/ui-components/src/components/ResultsCard.tsx b/frontend/shared/ui-components/src/components/ResultsCard.tsx index 5cc73c7..d048ae6 100644 --- a/frontend/shared/ui-components/src/components/ResultsCard.tsx +++ b/frontend/shared/ui-components/src/components/ResultsCard.tsx @@ -54,7 +54,10 @@ const ResultsCard: Component = (props) => { Taux de réussite : {last().successRate}% Durée de session : {formatDuration(last().durationSec)} - Position leaderboard : {last().leaderboardPosition != null ? `#${last().leaderboardPosition}` : 'Hors top 10'} + Position leaderboard :{' '} + {last().leaderboardPosition != null + ? `#${last().leaderboardPosition}` + : 'Hors top 10'} diff --git a/frontend/shared/ui-components/src/components/ScoreDisplay.tsx b/frontend/shared/ui-components/src/components/ScoreDisplay.tsx index 337e41c..fd12ad5 100644 --- a/frontend/shared/ui-components/src/components/ScoreDisplay.tsx +++ b/frontend/shared/ui-components/src/components/ScoreDisplay.tsx @@ -3,7 +3,13 @@ import type { Component } from 'solid-js' import Chip from '@suid/material/Chip' const ScoreDisplay: Component<{ score: number; labelPrefix?: string }> = (props) => { - return + return ( + + ) } export default ScoreDisplay diff --git a/frontend/shared/ui-components/src/components/ThemeBadge.tsx b/frontend/shared/ui-components/src/components/ThemeBadge.tsx index a662b00..56a2624 100644 --- a/frontend/shared/ui-components/src/components/ThemeBadge.tsx +++ b/frontend/shared/ui-components/src/components/ThemeBadge.tsx @@ -3,7 +3,8 @@ import type { Component } from 'solid-js' import Chip from '@suid/material/Chip' const ThemeBadge: Component<{ theme: string; labelPrefix?: string }> = (props) => { - const label = () => (props.labelPrefix ? `${props.labelPrefix}${props.theme}` : props.theme) + const label = (): string => + props.labelPrefix ? `${props.labelPrefix}${props.theme}` : props.theme return } diff --git a/frontend/shared/ui-components/src/components/Timer.tsx b/frontend/shared/ui-components/src/components/Timer.tsx index 77fe806..9234768 100644 --- a/frontend/shared/ui-components/src/components/Timer.tsx +++ b/frontend/shared/ui-components/src/components/Timer.tsx @@ -19,12 +19,19 @@ export type TimerProps = { } const Timer: Component = (props) => { - const mergedWarningText = () => ({ ...DEFAULT_TIMER_WARNING_TEXT, ...(props.warningText ?? {}) }) - const warning = () => getTimerWarning(props.remainingMs, mergedWarningText()) + const mergedWarningText = (): TimerWarningText => ({ + ...DEFAULT_TIMER_WARNING_TEXT, + ...(props.warningText ?? {}), + }) + const warning = (): string | null => getTimerWarning(props.remainingMs, mergedWarningText()) return ( - + {warning()} diff --git a/infrastructure/prod/.env.prod.example b/infrastructure/prod/.env.prod.example new file mode 100644 index 0000000..a67093a --- /dev/null +++ b/infrastructure/prod/.env.prod.example @@ -0,0 +1,105 @@ +# Runtime environment +ENVIRONMENT=production +LOG_LEVEL=info + +# PostgreSQL +POSTGRES_USER=knowfoolery +POSTGRES_PASSWORD=change-me +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_DB=knowfoolery +POSTGRES_SSLMODE=disable +POSTGRES_MAX_OPEN_CONNS=25 +POSTGRES_MAX_IDLE_CONNS=10 +POSTGRES_CONN_MAX_LIFETIME=5m +POSTGRES_CONN_MAX_IDLE_TIME=1m + +# Redis +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD= +REDIS_DB=0 +REDIS_POOL_SIZE=10 +REDIS_MIN_IDLE_CONNS=5 +REDIS_DIAL_TIMEOUT=5s +REDIS_READ_TIMEOUT=3s +REDIS_WRITE_TIMEOUT=3s + +# Service ports +GAME_SESSION_PORT=8080 +QUESTION_BANK_PORT=8081 +USER_SERVICE_PORT=8082 +LEADERBOARD_PORT=8083 +ADMIN_SERVICE_PORT=8085 +GATEWAY_PORT=8086 +GATEWAY_INTERNAL_PORT=18086 + +# Gateway +GATEWAY_PUBLIC_PREFIX=/api/v1 +GATEWAY_ALLOWED_ORIGINS=https://app.knowfoolery.com +GATEWAY_ALLOWED_METHODS=GET,POST,PUT,DELETE,OPTIONS +GATEWAY_ALLOWED_HEADERS=Origin,Content-Type,Accept,Authorization +GATEWAY_ALLOW_CREDENTIALS=true +GATEWAY_CORS_MAX_AGE_SECONDS=300 +GATEWAY_UPSTREAM_TIMEOUT=3s +GATEWAY_RATE_WINDOW=1m +GATEWAY_RATE_GENERAL=100 +GATEWAY_RATE_AUTH=5 +GATEWAY_RATE_API=60 +GATEWAY_RATE_ADMIN=30 +GATEWAY_CSP=default-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self' +GATEWAY_ENABLE_HSTS=true +GATEWAY_HSTS_MAX_AGE=31536000 +GATEWAY_FRAME_OPTIONS=DENY +GATEWAY_CONTENT_TYPE_OPTIONS=true +GATEWAY_REFERRER_POLICY=strict-origin-when-cross-origin +GATEWAY_PERMISSIONS_POLICY=geolocation=(), microphone=(), camera=(), payment=(), usb=() + +# Inter-service HTTP timeout +UPSTREAM_HTTP_TIMEOUT=3s + +# Feature configs +QUESTION_CACHE_TTL=5m +QUESTION_RANDOM_MAX_EXCLUSIONS=200 +QUESTION_BULK_MAX_ITEMS=5000 +GAME_SESSION_DURATION=30m +GAME_SESSION_MAX_ATTEMPTS=3 +GAME_SESSION_MIN_ANSWER_LATENCY_MS=300 +GAME_SESSION_LOCK_TTL=3s +GAME_SESSION_ACTIVE_KEY_TTL=35m +GAME_SESSION_END_REASON_DEFAULT=abandoned +LEADERBOARD_TOP_LIMIT=10 +LEADERBOARD_PLAYER_HISTORY_DEFAULT_LIMIT=20 +LEADERBOARD_PLAYER_HISTORY_MAX_LIMIT=100 +LEADERBOARD_CACHE_TTL=60s +LEADERBOARD_UPDATE_REQUIRE_AUTH=true +USER_ADMIN_LIST_DEFAULT_LIMIT=50 +USER_ADMIN_LIST_MAX_LIMIT=200 +ADMIN_AUDIT_RETENTION_DAYS=90 + +# Auth (Zitadel) +ZITADEL_URL=https://auth.knowfoolery.com +ZITADEL_ISSUER=https://auth.knowfoolery.com +ZITADEL_AUDIENCE=knowfoolery-api +ZITADEL_CLIENT_ID=replace-me +ZITADEL_CLIENT_SECRET=replace-me + +# Observability +METRICS_ENABLED=true +METRICS_SERVICE_NAME=knowfoolery +TRACING_ENABLED=true +TRACING_SERVICE_NAME=knowfoolery +TRACING_SERVICE_VERSION=0.1.0 +TRACING_ENVIRONMENT=production +TRACING_OTLP_ENDPOINT=http://jaeger:4318/v1/traces +TRACING_JAEGER_ENDPOINT=http://jaeger:14268/api/traces +TRACING_SAMPLE_RATE=0.2 +PROMETHEUS_PORT=9090 +GRAFANA_PORT=3000 +JAEGER_UI_PORT=16686 +JAEGER_COLLECTOR_PORT=14268 +JAEGER_OTLP_GRPC_PORT=4317 +JAEGER_OTLP_HTTP_PORT=4318 +JAEGER_AGENT_PORT=6831 +GRAFANA_ADMIN_USER=admin +GRAFANA_ADMIN_PASSWORD=change-me diff --git a/infrastructure/prod/docker-compose.yml b/infrastructure/prod/docker-compose.yml new file mode 100644 index 0000000..c63fa5a --- /dev/null +++ b/infrastructure/prod/docker-compose.yml @@ -0,0 +1,329 @@ +services: + postgres: + image: postgres:15-alpine + container_name: knowfoolery-postgres + restart: unless-stopped + env_file: + - .env.prod + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + ports: + - "${POSTGRES_PORT}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ../dev/init-scripts:/docker-entrypoint-initdb.d:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - backend + + redis: + image: redis:7-alpine + container_name: knowfoolery-redis + restart: unless-stopped + env_file: + - .env.prod + ports: + - "${REDIS_PORT}:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - backend + + question-bank-service: + build: + context: ../.. + dockerfile: infrastructure/services/question-bank.Dockerfile + container_name: knowfoolery-question-bank-service + restart: unless-stopped + env_file: + - .env.prod + environment: + QUESTION_BANK_PORT: ${QUESTION_BANK_PORT} + POSTGRES_HOST: postgres + POSTGRES_DB: questions + REDIS_HOST: redis + TRACING_ENABLED: ${TRACING_ENABLED} + TRACING_OTLP_ENDPOINT: ${TRACING_OTLP_ENDPOINT} + TRACING_JAEGER_ENDPOINT: ${TRACING_JAEGER_ENDPOINT} + METRICS_ENABLED: ${METRICS_ENABLED} + ports: + - "${QUESTION_BANK_PORT}:8081" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:${QUESTION_BANK_PORT}/health || exit 1"] + interval: 10s + timeout: 3s + retries: 10 + networks: + - backend + + user-service: + build: + context: ../.. + dockerfile: infrastructure/services/user.Dockerfile + container_name: knowfoolery-user-service + restart: unless-stopped + env_file: + - .env.prod + environment: + USER_SERVICE_PORT: ${USER_SERVICE_PORT} + POSTGRES_HOST: postgres + POSTGRES_DB: users + TRACING_ENABLED: ${TRACING_ENABLED} + TRACING_OTLP_ENDPOINT: ${TRACING_OTLP_ENDPOINT} + TRACING_JAEGER_ENDPOINT: ${TRACING_JAEGER_ENDPOINT} + METRICS_ENABLED: ${METRICS_ENABLED} + ports: + - "${USER_SERVICE_PORT}:8082" + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:${USER_SERVICE_PORT}/health || exit 1"] + interval: 10s + timeout: 3s + retries: 10 + networks: + - backend + + game-session-service: + build: + context: ../.. + dockerfile: infrastructure/services/game-session.Dockerfile + container_name: knowfoolery-game-session-service + restart: unless-stopped + env_file: + - .env.prod + environment: + GAME_SESSION_PORT: ${GAME_SESSION_PORT} + POSTGRES_HOST: postgres + POSTGRES_DB: game_sessions + REDIS_HOST: redis + QUESTION_BANK_BASE_URL: http://question-bank-service:${QUESTION_BANK_PORT} + USER_SERVICE_BASE_URL: http://user-service:${USER_SERVICE_PORT} + TRACING_ENABLED: ${TRACING_ENABLED} + TRACING_OTLP_ENDPOINT: ${TRACING_OTLP_ENDPOINT} + TRACING_JAEGER_ENDPOINT: ${TRACING_JAEGER_ENDPOINT} + METRICS_ENABLED: ${METRICS_ENABLED} + ports: + - "${GAME_SESSION_PORT}:8080" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + question-bank-service: + condition: service_healthy + user-service: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:${GAME_SESSION_PORT}/health || exit 1"] + interval: 10s + timeout: 3s + retries: 10 + networks: + - backend + + leaderboard-service: + build: + context: ../.. + dockerfile: infrastructure/services/leaderboard.Dockerfile + container_name: knowfoolery-leaderboard-service + restart: unless-stopped + env_file: + - .env.prod + environment: + LEADERBOARD_PORT: ${LEADERBOARD_PORT} + POSTGRES_HOST: postgres + POSTGRES_DB: leaderboards + REDIS_HOST: redis + TRACING_ENABLED: ${TRACING_ENABLED} + TRACING_OTLP_ENDPOINT: ${TRACING_OTLP_ENDPOINT} + TRACING_JAEGER_ENDPOINT: ${TRACING_JAEGER_ENDPOINT} + METRICS_ENABLED: ${METRICS_ENABLED} + ports: + - "${LEADERBOARD_PORT}:8083" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:${LEADERBOARD_PORT}/health || exit 1"] + interval: 10s + timeout: 3s + retries: 10 + networks: + - backend + + admin-service: + build: + context: ../.. + dockerfile: infrastructure/services/admin.Dockerfile + container_name: knowfoolery-admin-service + restart: unless-stopped + env_file: + - .env.prod + environment: + ADMIN_SERVICE_PORT: ${ADMIN_SERVICE_PORT} + POSTGRES_HOST: postgres + POSTGRES_DB: admin + TRACING_ENABLED: ${TRACING_ENABLED} + TRACING_OTLP_ENDPOINT: ${TRACING_OTLP_ENDPOINT} + TRACING_JAEGER_ENDPOINT: ${TRACING_JAEGER_ENDPOINT} + METRICS_ENABLED: ${METRICS_ENABLED} + ports: + - "${ADMIN_SERVICE_PORT}:8085" + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:${ADMIN_SERVICE_PORT}/health || exit 1"] + interval: 10s + timeout: 3s + retries: 10 + networks: + - backend + + gateway-service: + build: + context: ../.. + dockerfile: infrastructure/services/gateway.Dockerfile + container_name: knowfoolery-gateway-service + restart: unless-stopped + env_file: + - .env.prod + environment: + GATEWAY_INTERNAL_PORT: ${GATEWAY_INTERNAL_PORT} + REDIS_HOST: redis + GAME_SESSION_BASE_URL: http://game-session-service:${GAME_SESSION_PORT} + QUESTION_BANK_BASE_URL: http://question-bank-service:${QUESTION_BANK_PORT} + USER_SERVICE_BASE_URL: http://user-service:${USER_SERVICE_PORT} + LEADERBOARD_BASE_URL: http://leaderboard-service:${LEADERBOARD_PORT} + ADMIN_SERVICE_BASE_URL: http://admin-service:${ADMIN_SERVICE_PORT} + TRACING_ENABLED: ${TRACING_ENABLED} + TRACING_OTLP_ENDPOINT: ${TRACING_OTLP_ENDPOINT} + TRACING_JAEGER_ENDPOINT: ${TRACING_JAEGER_ENDPOINT} + METRICS_ENABLED: ${METRICS_ENABLED} + ports: + - "${GATEWAY_INTERNAL_PORT}:${GATEWAY_INTERNAL_PORT}" + depends_on: + redis: + condition: service_healthy + game-session-service: + condition: service_healthy + question-bank-service: + condition: service_healthy + user-service: + condition: service_healthy + leaderboard-service: + condition: service_healthy + admin-service: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:${GATEWAY_INTERNAL_PORT}/health || exit 1"] + interval: 10s + timeout: 3s + retries: 10 + networks: + - backend + + nginx: + image: nginx:1.27-alpine + container_name: knowfoolery-nginx + restart: unless-stopped + ports: + - "${GATEWAY_PORT}:8086" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + gateway-service: + condition: service_healthy + networks: + - public + - backend + + prometheus: + image: prom/prometheus:latest + container_name: knowfoolery-prometheus + restart: unless-stopped + env_file: + - .env.prod + ports: + - "${PROMETHEUS_PORT}:9090" + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.enable-lifecycle' + depends_on: + gateway-service: + condition: service_healthy + networks: + - backend + + grafana: + image: grafana/grafana:latest + container_name: knowfoolery-grafana + restart: unless-stopped + env_file: + - .env.prod + ports: + - "${GRAFANA_PORT}:3000" + environment: + GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER} + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD} + GF_USERS_ALLOW_SIGN_UP: false + volumes: + - grafana_data:/var/lib/grafana + depends_on: + prometheus: + condition: service_started + networks: + - backend + + jaeger: + image: jaegertracing/all-in-one:latest + container_name: knowfoolery-jaeger + restart: unless-stopped + ports: + - "${JAEGER_UI_PORT}:16686" + - "${JAEGER_COLLECTOR_PORT}:14268" + - "${JAEGER_OTLP_GRPC_PORT}:4317" + - "${JAEGER_OTLP_HTTP_PORT}:4318" + - "${JAEGER_AGENT_PORT}:6831/udp" + environment: + COLLECTOR_ZIPKIN_HOST_PORT: ":9411" + networks: + - backend + +volumes: + postgres_data: + redis_data: + prometheus_data: + grafana_data: + +networks: + public: + driver: bridge + backend: + driver: bridge diff --git a/infrastructure/prod/nginx/conf.d/default.conf b/infrastructure/prod/nginx/conf.d/default.conf new file mode 100644 index 0000000..35e1671 --- /dev/null +++ b/infrastructure/prod/nginx/conf.d/default.conf @@ -0,0 +1,29 @@ +upstream gateway_upstream { + server gateway-service:18086; +} + +server { + listen 8086; + server_name _; + + location = /nginx/health { + access_log off; + add_header Content-Type text/plain; + return 200 'ok'; + } + + location / { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Request-ID $request_id; + + proxy_connect_timeout 3s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + + proxy_pass http://gateway_upstream; + } +} diff --git a/infrastructure/prod/nginx/nginx.conf b/infrastructure/prod/nginx/nginx.conf new file mode 100644 index 0000000..a2b1777 --- /dev/null +++ b/infrastructure/prod/nginx/nginx.conf @@ -0,0 +1,27 @@ +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + server_tokens off; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log warn; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/infrastructure/prod/prometheus/prometheus.yml b/infrastructure/prod/prometheus/prometheus.yml new file mode 100644 index 0000000..65c36d8 --- /dev/null +++ b/infrastructure/prod/prometheus/prometheus.yml @@ -0,0 +1,38 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['prometheus:9090'] + + - job_name: 'gateway-service' + metrics_path: /metrics + static_configs: + - targets: ['gateway-service:18086'] + + - job_name: 'game-session-service' + metrics_path: /metrics + static_configs: + - targets: ['game-session-service:8080'] + + - job_name: 'question-bank-service' + metrics_path: /metrics + static_configs: + - targets: ['question-bank-service:8081'] + + - job_name: 'user-service' + metrics_path: /metrics + static_configs: + - targets: ['user-service:8082'] + + - job_name: 'leaderboard-service' + metrics_path: /metrics + static_configs: + - targets: ['leaderboard-service:8083'] + + - job_name: 'admin-service' + metrics_path: /metrics + static_configs: + - targets: ['admin-service:8085'] diff --git a/infrastructure/services/admin.Dockerfile b/infrastructure/services/admin.Dockerfile new file mode 100644 index 0000000..481602c --- /dev/null +++ b/infrastructure/services/admin.Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.25-alpine AS builder + +WORKDIR /src +COPY backend ./backend +RUN cd backend/services/admin-service && go mod download && CGO_ENABLED=0 GOOS=linux go build -o /out/app ./cmd/main.go + +FROM alpine:3.21 +RUN apk add --no-cache ca-certificates wget && adduser -D -H -u 10001 app +WORKDIR /app +COPY --from=builder /out/app /app/app + +EXPOSE 8085 +HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=5 CMD wget -qO- http://127.0.0.1:8085/health || exit 1 + +USER app +ENTRYPOINT ["/app/app"] diff --git a/infrastructure/services/game-session.Dockerfile b/infrastructure/services/game-session.Dockerfile new file mode 100644 index 0000000..c232685 --- /dev/null +++ b/infrastructure/services/game-session.Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.25-alpine AS builder + +WORKDIR /src +COPY backend ./backend +RUN cd backend/services/game-session-service && go mod download && CGO_ENABLED=0 GOOS=linux go build -o /out/app ./cmd/main.go + +FROM alpine:3.21 +RUN apk add --no-cache ca-certificates wget && adduser -D -H -u 10001 app +WORKDIR /app +COPY --from=builder /out/app /app/app + +EXPOSE 8080 +HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=5 CMD wget -qO- http://127.0.0.1:8080/health || exit 1 + +USER app +ENTRYPOINT ["/app/app"] diff --git a/infrastructure/services/gateway.Dockerfile b/infrastructure/services/gateway.Dockerfile new file mode 100644 index 0000000..0d39d19 --- /dev/null +++ b/infrastructure/services/gateway.Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.25-alpine AS builder + +WORKDIR /src +COPY backend ./backend +RUN cd backend/services/gateway-service && go mod download && CGO_ENABLED=0 GOOS=linux go build -o /out/app ./cmd/main.go + +FROM alpine:3.21 +RUN apk add --no-cache ca-certificates wget && adduser -D -H -u 10001 app +WORKDIR /app +COPY --from=builder /out/app /app/app + +EXPOSE 18086 +HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=5 CMD wget -qO- http://127.0.0.1:18086/health || exit 1 + +USER app +ENTRYPOINT ["/app/app"] diff --git a/infrastructure/services/leaderboard.Dockerfile b/infrastructure/services/leaderboard.Dockerfile new file mode 100644 index 0000000..a83f272 --- /dev/null +++ b/infrastructure/services/leaderboard.Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.25-alpine AS builder + +WORKDIR /src +COPY backend ./backend +RUN cd backend/services/leaderboard-service && go mod download && CGO_ENABLED=0 GOOS=linux go build -o /out/app ./cmd/main.go + +FROM alpine:3.21 +RUN apk add --no-cache ca-certificates wget && adduser -D -H -u 10001 app +WORKDIR /app +COPY --from=builder /out/app /app/app + +EXPOSE 8083 +HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=5 CMD wget -qO- http://127.0.0.1:8083/health || exit 1 + +USER app +ENTRYPOINT ["/app/app"] diff --git a/infrastructure/services/question-bank.Dockerfile b/infrastructure/services/question-bank.Dockerfile new file mode 100644 index 0000000..d3983ca --- /dev/null +++ b/infrastructure/services/question-bank.Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.25-alpine AS builder + +WORKDIR /src +COPY backend ./backend +RUN cd backend/services/question-bank-service && go mod download && CGO_ENABLED=0 GOOS=linux go build -o /out/app ./cmd/main.go + +FROM alpine:3.21 +RUN apk add --no-cache ca-certificates wget && adduser -D -H -u 10001 app +WORKDIR /app +COPY --from=builder /out/app /app/app + +EXPOSE 8081 +HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=5 CMD wget -qO- http://127.0.0.1:8081/health || exit 1 + +USER app +ENTRYPOINT ["/app/app"] diff --git a/infrastructure/services/user.Dockerfile b/infrastructure/services/user.Dockerfile new file mode 100644 index 0000000..616e177 --- /dev/null +++ b/infrastructure/services/user.Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.25-alpine AS builder + +WORKDIR /src +COPY backend ./backend +RUN cd backend/services/user-service && go mod download && CGO_ENABLED=0 GOOS=linux go build -o /out/app ./cmd/main.go + +FROM alpine:3.21 +RUN apk add --no-cache ca-certificates wget && adduser -D -H -u 10001 app +WORKDIR /app +COPY --from=builder /out/app /app/app + +EXPOSE 8082 +HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=5 CMD wget -qO- http://127.0.0.1:8082/health || exit 1 + +USER app +ENTRYPOINT ["/app/app"] diff --git a/tasks/deploy-dev.yml b/tasks/deploy-dev.yml new file mode 100644 index 0000000..a754005 --- /dev/null +++ b/tasks/deploy-dev.yml @@ -0,0 +1,49 @@ +version: '3' + +includes: + prod: + taskfile: ./deploy-prod.yml + flatten: true + +tasks: + deploy-dev: + desc: Deploy develop branch to development host via SSH + Docker Compose + cmds: + - | + set -eu + : "${DEPLOY_HOST:?DEPLOY_HOST is required}" + : "${DEPLOY_USER:?DEPLOY_USER is required}" + : "${DEPLOY_PATH:?DEPLOY_PATH is required}" + : "${DEPLOY_REF:?DEPLOY_REF is required}" + + ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "${DEPLOY_USER}@${DEPLOY_HOST}" " + set -euo pipefail + cd '${DEPLOY_PATH}' + PREV_REF=\$(git rev-parse HEAD) + + rollback() { + echo 'Deployment failed, rolling back to previous ref' >&2 + git checkout -f \"\${PREV_REF}\" + docker compose -f infrastructure/prod/docker-compose.yml --env-file infrastructure/prod/.env.prod up -d --build + } + + trap rollback ERR + + git fetch --all --tags --prune + git checkout -f '${DEPLOY_REF}' + git pull --ff-only origin '${DEPLOY_REF}' + + docker compose -f infrastructure/prod/docker-compose.yml --env-file infrastructure/prod/.env.prod config > /tmp/knowfoolery-compose-dev.txt + docker compose -f infrastructure/prod/docker-compose.yml --env-file infrastructure/prod/.env.prod build + docker compose -f infrastructure/prod/docker-compose.yml --env-file infrastructure/prod/.env.prod up -d + + for target in 8080 8081 8082 8083 8085 18086; do + curl -fsS \"http://localhost:\\${target}/health\" > /tmp/knowfoolery-health-\\${target}.txt + curl -fsS \"http://localhost:\\${target}/ready\" > /tmp/knowfoolery-ready-\\${target}.txt + done + + curl -fsS http://localhost:8086/health > /tmp/knowfoolery-smoke-health.txt + curl -fsS http://localhost:8086/ready > /tmp/knowfoolery-smoke-ready.txt + curl -fsS http://localhost:8086/api/v1/leaderboard/top10 > /tmp/knowfoolery-smoke-top10.txt + curl -fsS http://localhost:8086/api/v1/leaderboard/stats > /tmp/knowfoolery-smoke-stats.txt + " diff --git a/tasks/deploy-prod.yml b/tasks/deploy-prod.yml new file mode 100644 index 0000000..a7a724a --- /dev/null +++ b/tasks/deploy-prod.yml @@ -0,0 +1,43 @@ +version: '3' + +tasks: + deploy-prod: + desc: Deploy release tag to production host via SSH + Docker Compose + cmds: + - | + set -eu + : "${DEPLOY_HOST:?DEPLOY_HOST is required}" + : "${DEPLOY_USER:?DEPLOY_USER is required}" + : "${DEPLOY_PATH:?DEPLOY_PATH is required}" + : "${DEPLOY_REF:?DEPLOY_REF is required}" + + ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "${DEPLOY_USER}@${DEPLOY_HOST}" " + set -euo pipefail + cd '${DEPLOY_PATH}' + PREV_REF=\$(git rev-parse HEAD) + + rollback() { + echo 'Deployment failed, rolling back to previous ref' >&2 + git checkout -f \"\${PREV_REF}\" + docker compose -f infrastructure/prod/docker-compose.yml --env-file infrastructure/prod/.env.prod up -d --build + } + + trap rollback ERR + + git fetch --all --tags --prune + git checkout -f '${DEPLOY_REF}' + + docker compose -f infrastructure/prod/docker-compose.yml --env-file infrastructure/prod/.env.prod config > /tmp/knowfoolery-compose-prod.txt + docker compose -f infrastructure/prod/docker-compose.yml --env-file infrastructure/prod/.env.prod build + docker compose -f infrastructure/prod/docker-compose.yml --env-file infrastructure/prod/.env.prod up -d + + for target in 8080 8081 8082 8083 8085 18086; do + curl -fsS \"http://localhost:\\${target}/health\" > /tmp/knowfoolery-health-\\${target}.txt + curl -fsS \"http://localhost:\\${target}/ready\" > /tmp/knowfoolery-ready-\\${target}.txt + done + + curl -fsS http://localhost:8086/health > /tmp/knowfoolery-smoke-health.txt + curl -fsS http://localhost:8086/ready > /tmp/knowfoolery-smoke-ready.txt + curl -fsS http://localhost:8086/api/v1/leaderboard/top10 > /tmp/knowfoolery-smoke-top10.txt + curl -fsS http://localhost:8086/api/v1/leaderboard/stats > /tmp/knowfoolery-smoke-stats.txt + " diff --git a/tasks/security-scan.yml b/tasks/security-scan.yml new file mode 100644 index 0000000..e749ba1 --- /dev/null +++ b/tasks/security-scan.yml @@ -0,0 +1,75 @@ +version: '3' + +tasks: + security-scan: + desc: Run full CI quality and security checks + cmds: + - task: prepare-reports + - task: backend-lint + - task: frontend-lint + - task: unit-tests + - task: integration-tests + - task: docker-build-validate + - task: gosec-scan + - task: trivy-fs-scan + - task: trivy-image-scan + + prepare-reports: + internal: true + cmds: + - mkdir -p reports/security reports/tests reports/docker + + backend-lint: + cmds: + - | + set -eu + for module in \ + services/admin-service \ + services/game-session-service \ + services/gateway-service \ + services/leaderboard-service \ + services/question-bank-service \ + services/user-service \ + shared + do + (cd "backend/${module}" && golangci-lint run ./...) + done + + frontend-lint: + cmds: + - cd frontend && yarn lint + - cd frontend && yarn format:check + + unit-tests: + cmds: + - bash -o pipefail -c 'set -eu; for module in services/admin-service services/game-session-service services/gateway-service services/leaderboard-service services/question-bank-service services/user-service shared; do (cd "backend/${module}" && go test -v -race -cover ./...); done | tee reports/tests/backend-unit.log' + - bash -o pipefail -c 'cd frontend && CI=1 yarn test | tee ../reports/tests/frontend-unit.log' + + integration-tests: + cmds: + - bash -o pipefail -c 'set -eu; cd backend; for dir in services/*/tests; do if [ -d "$dir" ]; then go test -v "./$dir/..." | tee "../reports/tests/$(basename "$(dirname "$dir")")-integration.log"; fi; done' + + docker-build-validate: + cmds: + - bash -o pipefail -c 'set -eu; for service in gateway game-session question-bank user leaderboard admin; do docker build -f "infrastructure/services/${service}.Dockerfile" -t "knowfoolery/${service}:ci" . | tee "reports/docker/${service}-build.log"; done' + + gosec-scan: + cmds: + - bash -o pipefail -c 'set -eu; mkdir -p reports/security; set +e; gosec -fmt sarif -out reports/security/gosec.sarif ./backend/services/admin-service/... ./backend/services/game-session-service/... ./backend/services/gateway-service/... ./backend/services/leaderboard-service/... ./backend/services/question-bank-service/... ./backend/services/user-service/... ./backend/shared/... 2>&1 | tee reports/security/gosec.log; status=${PIPESTATUS[0]}; set -e; if grep -q "Panic when running SSA analyzer" reports/security/gosec.log || grep -q "file requires newer Go version" reports/security/gosec.log; then echo "gosec runtime/toolchain panic detected; treating as non-blocking tool failure."; exit 0; fi; exit "${status}"' + + trivy-fs-scan: + cmds: + - trivy fs --format json --output reports/security/trivy-fs.json --severity HIGH,CRITICAL --exit-code 1 . + + trivy-image-scan: + cmds: + - | + set -eu + for service in gateway game-session question-bank user leaderboard admin; do + trivy image \ + --format json \ + --output "reports/security/trivy-image-${service}.json" \ + --severity HIGH,CRITICAL \ + --exit-code 1 \ + "knowfoolery/${service}:ci" + done