Finished Phase 5: Infrastructure (Weeks 8-9)

master
oabrivard 4 months ago
parent 31167160f2
commit 2f283469c2

@ -0,0 +1,15 @@
.git
.gitignore
.vscode
**/.DS_Store
**/node_modules
**/dist
**/coverage
**/.cache
**/*.log
docs
frontend
infrastructure/dev
infrastructure/prod
.tmp
tmp

@ -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

@ -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

@ -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

4
.gitignore vendored

@ -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/

@ -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
# =============================================================================

@ -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

@ -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

@ -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

@ -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=

@ -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

@ -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=

@ -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

@ -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=

@ -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

@ -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=

@ -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

@ -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=

@ -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

@ -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=

@ -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

@ -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=

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />

@ -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}]]}
{"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}]]}

@ -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<string | null>(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,
}

@ -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<number>
isExpired: Accessor<boolean>
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

@ -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', () => {

@ -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<number | null> {
async function estimateLeaderboardPosition(
score: number,
durationSec: number
): Promise<number | null> {
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<void> => {
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}`)

@ -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<string | null>(null)
const start = () => {
const start = (): void => {
const name = playerName().trim()
const validationError = validatePlayerName(name)
if (validationError) {
@ -42,9 +42,7 @@ export default function HomeRoute() {
<Typography variant="h4" sx={{ fontWeight: 800 }}>
Bienvenue
</Typography>
<Typography sx={{ opacity: 0.8 }}>
Entre ton pseudo et lance une partie.
</Typography>
<Typography sx={{ opacity: 0.8 }}>Entre ton pseudo et lance une partie.</Typography>
<TextField
label="Nom de joueur"

@ -1,4 +1,4 @@
import { createResource } from 'solid-js'
import { createResource, type JSX } from 'solid-js'
import { LeaderboardTable } from '@knowfoolery/ui-components'
import Box from '@suid/material/Box'
@ -10,7 +10,7 @@ import Typography from '@suid/material/Typography'
import { leaderboardClient } from '../services/api'
export default function LeaderboardRoute() {
export default function LeaderboardRoute(): JSX.Element {
const [items, { refetch }] = createResource(async () => leaderboardClient.top10())
return (

@ -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() {
<Typography variant="h4" sx={{ fontWeight: 800 }}>
Profil
</Typography>
<Typography sx={{ opacity: 0.8 }}>Connexion requise pour accéder au profil joueur.</Typography>
<Typography sx={{ opacity: 0.8 }}>
Connexion requise pour accéder au profil joueur.
</Typography>
<Button variant="contained" onClick={signInDemo}>
Se connecter (mode démo)
</Button>
@ -78,13 +80,16 @@ export default function ProfileRoute() {
</Typography>
<Show
when={gameHistory().length > 0}
fallback={<Typography sx={{ opacity: 0.8 }}>Aucune partie enregistrée.</Typography>}
fallback={
<Typography sx={{ opacity: 0.8 }}>Aucune partie enregistrée.</Typography>
}
>
<For each={gameHistory().slice(0, 5)}>
{(item) => (
<Stack spacing={0.5}>
<Typography>
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
</Typography>
<Divider />
</Stack>

@ -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', () => {

@ -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())

@ -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'
)
})
})

@ -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
}

@ -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';
}

@ -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}]]}
{"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}]]}

@ -12,10 +12,11 @@ export type AttemptIndicatorProps = {
}
const AttemptIndicator: Component<AttemptIndicatorProps> = (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 (
<Stack direction="row" spacing={2} alignItems="center">

@ -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)
})
})

@ -21,7 +21,7 @@ export type HintButtonProps = {
const HintButton: Component<HintButtonProps> = (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<HintButtonProps> = (props) => {
setDialogOpen(true)
}
const confirm = () => {
const confirm = (): void => {
setDialogOpen(false)
props.onConfirm()
}
@ -44,10 +44,11 @@ const HintButton: Component<HintButtonProps> = (props) => {
</Button>
<Dialog open={dialogOpen()} onClose={() => setDialogOpen(false)}>
<DialogTitle>{props.confirmTitle ?? 'Confirmer l\'indice'}</DialogTitle>
<DialogTitle>{props.confirmTitle ?? "Confirmer l'indice"}</DialogTitle>
<DialogContent>
<Typography>
{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 ?'}
</Typography>
</DialogContent>
<DialogActions>

@ -24,11 +24,18 @@ export type LeaderboardTableProps = {
}
const LeaderboardTable: Component<LeaderboardTableProps> = (props) => {
const maxRows = () => Math.max(1, props.maxRows ?? 10)
const maxRows = (): number => Math.max(1, props.maxRows ?? 10)
return (
<Show when={!props.loading} fallback={<CircularProgress />}>
<Show when={props.rows.length > 0} fallback={<Typography sx={{ opacity: 0.8 }}>{props.emptyMessage ?? 'Aucun score pour le moment.'}</Typography>}>
<Show
when={props.rows.length > 0}
fallback={
<Typography sx={{ opacity: 0.8 }}>
{props.emptyMessage ?? 'Aucun score pour le moment.'}
</Typography>
}
>
<Table size="small" aria-label={props.ariaLabel ?? 'top-10-leaderboard'}>
<TableHead>
<TableRow>

@ -30,7 +30,12 @@ describe('ResultsCard', () => {
it('renders fallback when result is null', () => {
render(() => (
<ResultsCard result={null} onPlayAgain={vi.fn()} onViewLeaderboard={vi.fn()} onStartGame={vi.fn()} />
<ResultsCard
result={null}
onPlayAgain={vi.fn()}
onViewLeaderboard={vi.fn()}
onStartGame={vi.fn()}
/>
))
expect(screen.getByText('Aucune partie terminée pour le moment.')).toBeTruthy()

@ -54,7 +54,10 @@ const ResultsCard: Component<ResultsCardProps> = (props) => {
<Typography>Taux de réussite : {last().successRate}%</Typography>
<Typography>Durée de session : {formatDuration(last().durationSec)}</Typography>
<Typography>
Position leaderboard : {last().leaderboardPosition != null ? `#${last().leaderboardPosition}` : 'Hors top 10'}
Position leaderboard :{' '}
{last().leaderboardPosition != null
? `#${last().leaderboardPosition}`
: 'Hors top 10'}
</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>

@ -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 <Chip label={`${props.labelPrefix ?? 'Score:'} ${props.score}`} color="primary" variant="outlined" />
return (
<Chip
label={`${props.labelPrefix ?? 'Score:'} ${props.score}`}
color="primary"
variant="outlined"
/>
)
}
export default ScoreDisplay

@ -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 <Chip label={label()} variant="outlined" color="secondary" />
}

@ -19,12 +19,19 @@ export type TimerProps = {
}
const Timer: Component<TimerProps> = (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 (
<Stack spacing={0.5} alignItems="flex-end">
<Chip label={formatMs(props.remainingMs)} color={getTimerColor(props.remainingMs)} variant="outlined" />
<Chip
label={formatMs(props.remainingMs)}
color={getTimerColor(props.remainingMs)}
variant="outlined"
/>
<Show when={props.showWarning !== false && warning()}>
<Typography variant="caption" sx={{ color: 'warning.main', fontWeight: 700 }}>
{warning()}

@ -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

@ -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

@ -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;
}
}

@ -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;
}

@ -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']

@ -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"]

@ -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"]

@ -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"]

@ -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"]

@ -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"]

@ -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"]

@ -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
"

@ -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
"

@ -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
Loading…
Cancel
Save