From 04ba3d541f6fc77c08c562a520d03c79f41de160 Mon Sep 17 00:00:00 2001 From: Lorenzo Sanesi Date: Mon, 10 Aug 2026 13:52:13 +0200 Subject: [PATCH] Docker compose con variabili d'ambiente --- .env.example | 56 ++++++++++ backend-node/config/database.js | 6 +- backend-node/controllers/users.controller.js | 2 +- .../repositories/postgres/users.repository.js | 21 ++++ backend-node/services/users.service.js | 11 ++ docker-compose.yml | 101 ++++++++++-------- 6 files changed, 147 insertions(+), 50 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..de51a32 --- /dev/null +++ b/.env.example @@ -0,0 +1,56 @@ +# ========================================================================== +# ScommettiApp - variabili d'ambiente per docker-compose (root) +# +# Copia questo file in ".env" nella stessa cartella e compila i valori. +# Il file ".env" reale NON va committato (è già in .gitignore). +# ========================================================================== + +# -------------------------------------------------------------------------- +# Rete / porte pubblicate sull'host +# -------------------------------------------------------------------------- +# IP su cui pubblicare le porte dei container sull'host della VPS. +# Caddy gira in un CT Proxmox separato (non nella stessa rete Docker), quindi +# deve raggiungere questi servizi via IP dell'host + porta pubblicata. +# Imposta qui l'IP con cui la VPS è raggiungibile dal CT di Caddy (es. IP +# della rete interna/bridge Proxmox), NON 0.0.0.0: pubblicare su 0.0.0.0 +# espone i servizi anche a internet, bypassando Caddy (e occhio: le regole +# iptables di Docker bypassano ufw se non configurato apposta - vedi note +# nel Caddyfile.example). +BIND_IP=10.0.0.10 + +BACKEND_PORT=3000 +FRONTEND_PORT=8081 +KEYCLOAK_PORT=8080 + +# -------------------------------------------------------------------------- +# Applicazione +# -------------------------------------------------------------------------- +NODE_ENV=production + +# -------------------------------------------------------------------------- +# PostgreSQL (dati app: users, groups, friendships, ...) +# -------------------------------------------------------------------------- +POSTGRES_DB=amici_scommesse +POSTGRES_USER=admin +POSTGRES_PASSWORD=change-me-strong-password + +# -------------------------------------------------------------------------- +# MongoDB (bets) +# -------------------------------------------------------------------------- +MONGO_INITDB_ROOT_USERNAME=root +MONGO_INITDB_ROOT_PASSWORD=change-me-strong-password +MONGO_INITDB_DATABASE=amici-scommesse + +# -------------------------------------------------------------------------- +# Keycloak +# -------------------------------------------------------------------------- +# Dominio pubblico con cui Caddy espone l'app (usato da Keycloak per +# costruire URL assoluti corretti dietro il reverse proxy). +DOMAIN=scommetti.example.com + +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=change-me-strong-password + +KEYCLOAK_DB=keycloak +KEYCLOAK_DB_USER=keycloak +KEYCLOAK_DB_PASSWORD=change-me-strong-password diff --git a/backend-node/config/database.js b/backend-node/config/database.js index 7309dce..b6be069 100644 --- a/backend-node/config/database.js +++ b/backend-node/config/database.js @@ -19,10 +19,6 @@ module.exports = { uri: process.env.MONGO_URI || 'mongodb://localhost:27017/amici-scommesse', - options: { - user: 'root', - pass: 'root@servovio', - authSource: 'admin', - }, + options: {}, }, }; diff --git a/backend-node/controllers/users.controller.js b/backend-node/controllers/users.controller.js index 33d7612..1e86bd5 100644 --- a/backend-node/controllers/users.controller.js +++ b/backend-node/controllers/users.controller.js @@ -7,7 +7,7 @@ class UsersController { async getCurrentUser(req, res, next) { try { - const user = await this.usersService.getUserById(req.userId); + const user = await this.usersService.getCurrentUserWithRank(req.userId); return ResponseHandler.success(res, user, 'User retrieved successfully'); } catch (error) { next(error); diff --git a/backend-node/repositories/postgres/users.repository.js b/backend-node/repositories/postgres/users.repository.js index eb03d0f..c02228a 100644 --- a/backend-node/repositories/postgres/users.repository.js +++ b/backend-node/repositories/postgres/users.repository.js @@ -32,6 +32,27 @@ class UsersRepository { } } + async count() { + try { + return await this.User.count(); + } catch (error) { + throw new DatabaseError('Failed to count users', error); + } + } + + async countWithScoreAbove(score, excludeId) { + try { + return await this.User.count({ + where: { + score: { [Op.gt]: score }, + id: { [Op.ne]: excludeId }, + }, + }); + } catch (error) { + throw new DatabaseError('Failed to count users above score', error); + } + } + async findById(id, options = {}) { try { const user = await this.User.findByPk(id, options); diff --git a/backend-node/services/users.service.js b/backend-node/services/users.service.js index af4fb2f..8d1a066 100644 --- a/backend-node/services/users.service.js +++ b/backend-node/services/users.service.js @@ -13,6 +13,17 @@ class UsersService { return await this.usersRepository.findById(id); } + async getCurrentUserWithRank(id) { + const user = await this.usersRepository.findById(id); + const userJson = user.toJSON ? user.toJSON() : { ...user }; + const totalUsers = await this.usersRepository.count(); + const usersAbove = await this.usersRepository.countWithScoreAbove(userJson.score, id); + const rankPercentile = totalUsers > 1 + ? Math.max(1, Math.round((usersAbove / totalUsers) * 100)) + : 1; + return { ...userJson, rankPercentile }; + } + async searchUsers(query, excludeUserId) { return await this.usersRepository.search(query, excludeUserId); } diff --git a/docker-compose.yml b/docker-compose.yml index 61de0eb..d3dc80f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,37 +1,45 @@ version: '3.8' +# Stack di produzione per la VPS, pensato per stare dietro a Caddy che gira +# su un CT Proxmox separato (routing per path sullo stesso dominio: "/" al +# frontend, "/api" al backend, "/auth" a Keycloak - vedi Caddyfile.example). +# +# Le variabili sono lette da un file ".env" nella stessa cartella +# (copia .env.example -> .env e compila i valori prima di avviare lo stack). +# +# Solo backend, frontend e keycloak pubblicano una porta sull'host (quella +# che deve raggiungere Caddy): postgres, mongodb e keycloak-db restano +# raggiungibili solo dalla rete Docker interna. + services: - # PostgreSQL Database + # PostgreSQL Database (dati app) postgres: image: postgres:15-alpine container_name: scommetti-postgres environment: - POSTGRES_DB: amici_scommesse - POSTGRES_USER: admin - POSTGRES_PASSWORD: admin - ports: - - "5432:5432" + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data - ./backend-node/init.sql:/docker-entrypoint-initdb.d/init.sql networks: - scommetti-network healthcheck: - test: ["CMD-SHELL", "pg_isready -U admin -d amici_scommesse"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 + restart: unless-stopped - # MongoDB Database + # MongoDB Database (bets) mongodb: image: mongo:7 container_name: scommetti-mongodb environment: - MONGO_INITDB_ROOT_USERNAME: root - MONGO_INITDB_ROOT_PASSWORD: root@servovio - MONGO_INITDB_DATABASE: amici-scommesse - ports: - - "27017:27017" + MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD} + MONGO_INITDB_DATABASE: ${MONGO_INITDB_DATABASE} volumes: - mongodb_data:/data/db networks: @@ -41,43 +49,53 @@ services: interval: 10s timeout: 5s retries: 5 + restart: unless-stopped # Keycloak Database keycloak-db: image: postgres:15-alpine container_name: scommetti-keycloak-db environment: - POSTGRES_DB: keycloak - POSTGRES_USER: keycloak - POSTGRES_PASSWORD: keycloak + POSTGRES_DB: ${KEYCLOAK_DB} + POSTGRES_USER: ${KEYCLOAK_DB_USER} + POSTGRES_PASSWORD: ${KEYCLOAK_DB_PASSWORD} volumes: - keycloak_db_data:/var/lib/postgresql/data networks: - scommetti-network healthcheck: - test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"] + test: ["CMD-SHELL", "pg_isready -U ${KEYCLOAK_DB_USER} -d ${KEYCLOAK_DB}"] interval: 10s timeout: 5s retries: 5 + restart: unless-stopped # Keycloak Auth Server keycloak: image: quay.io/keycloak/keycloak:23.0.0 container_name: scommetti-keycloak - command: start-dev --import-realm + command: start --import-realm environment: KC_DB: postgres - KC_DB_URL: jdbc:postgresql://keycloak-db:5432/keycloak - KC_DB_USERNAME: keycloak - KC_DB_PASSWORD: keycloak - KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin + KC_DB_URL: jdbc:postgresql://keycloak-db:5432/${KEYCLOAK_DB} + KC_DB_USERNAME: ${KEYCLOAK_DB_USER} + KC_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD} + KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN} + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} KC_HEALTH_ENABLED: "true" KC_METRICS_ENABLED: "true" - KC_HOSTNAME_STRICT: "false" KC_HTTP_ENABLED: "true" + # Keycloak è raggiunto tramite Caddy su https://${DOMAIN}/auth/*: gli + # diciamo di servire tutto sotto /auth e di fidarsi degli header + # X-Forwarded-* impostati dal proxy (TLS termina su Caddy, "edge"). + KC_HTTP_RELATIVE_PATH: /auth + KC_HOSTNAME: ${DOMAIN} + KC_HOSTNAME_PATH: /auth + KC_HOSTNAME_STRICT: "true" + KC_HOSTNAME_STRICT_HTTPS: "true" + KC_PROXY: edge ports: - - "8080:8080" + - "${BIND_IP}:${KEYCLOAK_PORT}:8080" volumes: - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm.json:ro - ./keycloak/themes/scommettiapp:/opt/keycloak/themes/scommettiapp:ro @@ -87,29 +105,30 @@ services: networks: - scommetti-network healthcheck: - test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && printf 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && timeout 5 cat <&3 | grep -q 'UP'"] + # Con KC_HTTP_RELATIVE_PATH=/auth l'health endpoint si sposta sotto + # /auth/health/ready (verificato: /health/ready da solo risponde 404). + test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && printf 'GET /auth/health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && timeout 5 cat <&3 | grep -q 'UP'"] interval: 15s timeout: 15s retries: 10 start_period: 90s + restart: unless-stopped # Backend Node.js backend: build: context: ./backend-node dockerfile: Dockerfile + target: production container_name: scommetti-backend environment: - NODE_ENV: development + NODE_ENV: ${NODE_ENV} PORT: 3000 - POSTGRES_URI: postgres://admin:admin@postgres:5432/amici_scommesse - MONGO_URI: mongodb://mongodb:27017/amici-scommesse - KEYCLOAK_JWKS_URI: http://keycloak:8080/realms/scommettiapp/protocol/openid-connect/certs + POSTGRES_URI: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} + MONGO_URI: mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongodb:27017/${MONGO_INITDB_DATABASE}?authSource=admin + KEYCLOAK_JWKS_URI: http://keycloak:8080/auth/realms/scommettiapp/protocol/openid-connect/certs ports: - - "3000:3000" - volumes: - - ./backend-node:/app - - /app/node_modules + - "${BIND_IP}:${BACKEND_PORT}:3000" depends_on: postgres: condition: service_healthy @@ -119,23 +138,17 @@ services: condition: service_healthy networks: - scommetti-network - command: npm start restart: unless-stopped - # Frontend Angular + # Frontend Angular (build statica servita da nginx) frontend: build: - context: ./frontend-angular-new + context: ./frontend-angular dockerfile: Dockerfile + target: production container_name: scommetti-frontend - environment: - NODE_ENV: development ports: - - "4200:4200" - volumes: - - ./frontend-angular-new:/app - - /app/node_modules - - /app/.angular + - "${BIND_IP}:${FRONTEND_PORT}:80" depends_on: - backend - keycloak