From 65abbff7edc1ca7b4a547dc314ad586997ffa5cc Mon Sep 17 00:00:00 2001 From: David Date: Tue, 23 Jun 2026 12:42:25 +0200 Subject: [PATCH] fix error preprod --- .../security/security.config.ts | 42 +- infra/preprod/docker-stack.preprod.yml | 402 ++++++++++++++++++ 2 files changed, 440 insertions(+), 4 deletions(-) create mode 100644 infra/preprod/docker-stack.preprod.yml diff --git a/apps/backend/src/infrastructure/security/security.config.ts b/apps/backend/src/infrastructure/security/security.config.ts index fd4ffef..e8908ef 100644 --- a/apps/backend/src/infrastructure/security/security.config.ts +++ b/apps/backend/src/infrastructure/security/security.config.ts @@ -95,11 +95,38 @@ export const rateLimitConfig = { }, }; +/** + * Allowed CORS origins. + * + * Reads CORS_ORIGIN (comma-separated list — supports several frontend hosts such + * as app.* and www.*), falling back to FRONTEND_URL, then to localhost in dev. + * With `credentials: true` the browser requires an exact origin echo (no `*`), + * so we resolve the request origin against this allow-list per request. + */ +const allowedOrigins: string[] = ( + process.env.CORS_ORIGIN || + process.env.FRONTEND_URL || + 'http://localhost:3000,http://localhost:3001' +) + .split(',') + .map(origin => origin.trim()) + .filter(Boolean); + /** * CORS Configuration */ export const corsConfig = { - origin: process.env.FRONTEND_URL || ['http://localhost:3000', 'http://localhost:3001'], + origin: ( + requestOrigin: string | undefined, + callback: (err: Error | null, allow?: boolean) => void + ): void => { + // Allow non-browser clients (no Origin header: server-to-server, curl, health checks). + if (!requestOrigin || allowedOrigins.includes(requestOrigin)) { + callback(null, true); + return; + } + callback(null, false); + }, credentials: true, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'X-CSRF-Token'], @@ -156,15 +183,22 @@ export const AUTH_COOKIE_NAMES = { export function authCookieOptions(options?: { maxAgeMs?: number; httpOnly?: boolean }): { httpOnly: boolean; secure: boolean; - sameSite: 'lax'; + sameSite: 'lax' | 'strict' | 'none'; path: string; domain?: string; maxAge?: number; } { + // SameSite must be 'none' when the frontend and the API live on different + // sites (cross-origin), otherwise the browser drops the auth cookies set in + // the cross-site login XHR response. 'none' REQUIRES Secure (HTTPS). + // Configurable via COOKIE_SAMESITE; defaults to 'lax' for same-site setups. + const sameSite = (process.env.COOKIE_SAMESITE?.toLowerCase() as 'lax' | 'strict' | 'none') || 'lax'; + const secure = process.env.NODE_ENV === 'production' || sameSite === 'none'; + return { httpOnly: options?.httpOnly ?? true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', + secure, + sameSite, path: '/', ...(process.env.COOKIE_DOMAIN ? { domain: process.env.COOKIE_DOMAIN } : {}), ...(options?.maxAgeMs ? { maxAge: options.maxAgeMs } : {}), diff --git a/infra/preprod/docker-stack.preprod.yml b/infra/preprod/docker-stack.preprod.yml new file mode 100644 index 0000000..df02e4f --- /dev/null +++ b/infra/preprod/docker-stack.preprod.yml @@ -0,0 +1,402 @@ +version: '3.8' + +services: + # PostgreSQL Database + xpeditis-db: + image: postgres:15-alpine + deploy: + restart_policy: + condition: on-failure + volumes: + - xpeditis_db_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: xpeditis_preprod + POSTGRES_USER: xpeditis + POSTGRES_PASSWORD: 9Lc3M9qoPBeHLKHDXGUf1 + PGDATA: /var/lib/postgresql/data/pgdata + healthcheck: + test: ["CMD-SHELL", "pg_isready -U xpeditis"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - xpeditis_internal + + # Redis Cache + xpeditis-redis: + image: redis:7-alpine + deploy: + restart_policy: + condition: on-failure + command: redis-server --requirepass hXiy5GMPswMtxMZujjS2O --appendonly yes + volumes: + - xpeditis_redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "hXiy5GMPswMtxMZujjS2O", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - xpeditis_internal + + # MinIO S3 Storage + xpeditis-minio: + image: minio/minio:latest + deploy: + restart_policy: + condition: on-failure + command: server /data --console-address ":9001" + volumes: + - xpeditis_minio_data:/data + environment: + MINIO_ROOT_USER: minioadmin_preprod_CHANGE_ME + MINIO_ROOT_PASSWORD: RBJfD0QVXC5JDfAHCwdUW + networks: + - xpeditis_internal + - traefik_network + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik_network" + + # MinIO API (S3) - HTTPS + - "traefik.http.routers.xpeditis-minio-api.rule=Host(`s3.preprod.xpeditis.com`)" + - "traefik.http.routers.xpeditis-minio-api.entrypoints=websecure" + - "traefik.http.routers.xpeditis-minio-api.tls=true" + - "traefik.http.routers.xpeditis-minio-api.tls.certresolver=letsencrypt" + - "traefik.http.routers.xpeditis-minio-api.priority=50" + - "traefik.http.routers.xpeditis-minio-api.service=xpeditis-minio-api" + - "traefik.http.services.xpeditis-minio-api.loadbalancer.server.port=9000" + - "traefik.http.routers.xpeditis-minio-api.middlewares=xpeditis-minio-api-headers" + + # MinIO API Headers + - "traefik.http.middlewares.xpeditis-minio-api-headers.headers.customRequestHeaders.X-Forwarded-Proto=https" + - "traefik.http.middlewares.xpeditis-minio-api-headers.headers.customRequestHeaders.X-Forwarded-For=" + - "traefik.http.middlewares.xpeditis-minio-api-headers.headers.customRequestHeaders.X-Real-IP=" + + # MinIO API - HTTP → HTTPS Redirect + - "traefik.http.routers.xpeditis-minio-api-http.rule=Host(`s3.preprod.xpeditis.com`)" + - "traefik.http.routers.xpeditis-minio-api-http.entrypoints=web" + - "traefik.http.routers.xpeditis-minio-api-http.priority=50" + - "traefik.http.routers.xpeditis-minio-api-http.middlewares=xpeditis-minio-api-redirect" + - "traefik.http.routers.xpeditis-minio-api-http.service=xpeditis-minio-api" + - "traefik.http.middlewares.xpeditis-minio-api-redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.xpeditis-minio-api-redirect.redirectscheme.permanent=true" + + # MinIO Console - HTTPS + - "traefik.http.routers.xpeditis-minio-console.rule=Host(`minio.preprod.xpeditis.com`)" + - "traefik.http.routers.xpeditis-minio-console.entrypoints=websecure" + - "traefik.http.routers.xpeditis-minio-console.tls=true" + - "traefik.http.routers.xpeditis-minio-console.tls.certresolver=letsencrypt" + - "traefik.http.routers.xpeditis-minio-console.priority=50" + - "traefik.http.routers.xpeditis-minio-console.service=xpeditis-minio-console" + - "traefik.http.services.xpeditis-minio-console.loadbalancer.server.port=9001" + - "traefik.http.routers.xpeditis-minio-console.middlewares=xpeditis-minio-console-headers" + + # MinIO Console Headers + - "traefik.http.middlewares.xpeditis-minio-console-headers.headers.customRequestHeaders.X-Forwarded-Proto=https" + - "traefik.http.middlewares.xpeditis-minio-console-headers.headers.customRequestHeaders.X-Forwarded-For=" + - "traefik.http.middlewares.xpeditis-minio-console-headers.headers.customRequestHeaders.X-Real-IP=" + + # MinIO Console - HTTP → HTTPS Redirect + - "traefik.http.routers.xpeditis-minio-console-http.rule=Host(`minio.preprod.xpeditis.com`)" + - "traefik.http.routers.xpeditis-minio-console-http.entrypoints=web" + - "traefik.http.routers.xpeditis-minio-console-http.priority=50" + - "traefik.http.routers.xpeditis-minio-console-http.middlewares=xpeditis-minio-console-redirect" + - "traefik.http.routers.xpeditis-minio-console-http.service=xpeditis-minio-console" + - "traefik.http.middlewares.xpeditis-minio-console-redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.xpeditis-minio-console-redirect.redirectscheme.permanent=true" + + # Backend API (NestJS) + xpeditis-backend: + image: rg.fr-par.scw.cloud/weworkstudio/xpeditis-backend:preprod + healthcheck: + disable: true + deploy: + restart_policy: + condition: on-failure + depends_on: + - xpeditis-db + - xpeditis-redis + labels: + - "logging=promtail" + - "logging.service=backend" + - "traefik.enable=true" + + # Backend API - HTTPS + - "traefik.http.routers.xpeditis-api.rule=Host(`api.preprod.xpeditis.com`)" + - "traefik.http.routers.xpeditis-api.entrypoints=websecure" + - "traefik.http.routers.xpeditis-api.tls=true" + - "traefik.http.routers.xpeditis-api.tls.certresolver=letsencrypt" + - "traefik.http.routers.xpeditis-api.priority=50" + - "traefik.http.routers.xpeditis-api.service=xpeditis-api" + - "traefik.http.services.xpeditis-api.loadbalancer.server.port=4000" + - "traefik.http.routers.xpeditis-api.middlewares=xpeditis-api-headers" + + # Backend API Headers + - "traefik.http.middlewares.xpeditis-api-headers.headers.customRequestHeaders.X-Forwarded-Proto=https" + - "traefik.http.middlewares.xpeditis-api-headers.headers.customRequestHeaders.X-Forwarded-For=" + - "traefik.http.middlewares.xpeditis-api-headers.headers.customRequestHeaders.X-Real-IP=" + + # Backend API - HTTP → HTTPS Redirect + - "traefik.http.routers.xpeditis-api-http.rule=Host(`api.preprod.xpeditis.com`)" + - "traefik.http.routers.xpeditis-api-http.entrypoints=web" + - "traefik.http.routers.xpeditis-api-http.priority=50" + - "traefik.http.routers.xpeditis-api-http.middlewares=xpeditis-api-redirect" + - "traefik.http.routers.xpeditis-api-http.service=xpeditis-api" + - "traefik.http.middlewares.xpeditis-api-redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.xpeditis-api-redirect.redirectscheme.permanent=true" + environment: + NODE_ENV: production + PORT: "4000" + API_PREFIX: api/v1 + LOG_FORMAT: json + + # Database + DATABASE_HOST: xpeditis-db + DATABASE_PORT: "5432" + DATABASE_USER: xpeditis + DATABASE_PASSWORD: 9Lc3M9qoPBeHLKHDXGUf1 + DATABASE_NAME: xpeditis_preprod + DATABASE_SYNC: "false" + DATABASE_LOGGING: "false" + + # Email (SMTP) + # SMTP (Brevo) + SMTP_HOST: smtp-relay.brevo.com + SMTP_PORT: 587 + SMTP_USER: 9637ef001@smtp-brevo.com + SMTP_PASS: xsmtpsib-8d965bda028cd63bed868a119f9e0330485204bf9f4e1f92a3a11c8e61000722-xUYUSrGGxhMqlUcu + SMTP_SECURE: "false" + SMTP_FROM: noreply@xpeditis.com + + # Redis + REDIS_HOST: xpeditis-redis + REDIS_PORT: "6379" + REDIS_PASSWORD: hXiy5GMPswMtxMZujjS2O + REDIS_DB: "0" + + # JWT + JWT_SECRET: 4C4tQC8qym/evv4zI5DaUE1yy3kilEnm6lApOGD0GgNBLA0BLm2tVyUr1Lr0mTnV + JWT_ACCESS_EXPIRATION: 15m + JWT_REFRESH_EXPIRATION: 7d + + # S3/MinIO + AWS_S3_ENDPOINT: http://xpeditis-minio:9000 + AWS_REGION: us-east-1 + AWS_ACCESS_KEY_ID: minioadmin_preprod_CHANGE_ME + AWS_SECRET_ACCESS_KEY: RBJfD0QVXC5JDfAHCwdUW + AWS_S3_BUCKET: xpeditis-csv-rates + + # CORS + CORS_ORIGIN: https://app.preprod.xpeditis.com,https://www.preprod.xpeditis.com,https://api.preprod.xpeditis.com + + # App URLs + APP_URL: https://app.preprod.xpeditis.com + FRONTEND_URL: https://app.preprod.xpeditis.com + API_URL: https://api.preprod.xpeditis.com + + # ─── Auth cookies (FIX login redirect) ────────────────────────────────── + # The httpOnly auth cookies (accessToken / refreshToken / xpeditis_session) + # MUST be shared across the parent domain so that: + # 1. the API (api.preprod.xpeditis.com) can set them, AND + # 2. the Next.js middleware running on the FRONTEND + # (app.preprod.xpeditis.com) can read `accessToken` to authorize routes. + # Without this, login succeeds but the middleware never sees the cookie and + # bounces the user back to /login instead of /dashboard. + # Leading dot => valid for every *.preprod.xpeditis.com subdomain. + COOKIE_DOMAIN: .preprod.xpeditis.com + # app & api are same-site (same xpeditis.com), so 'lax' is correct & safest. + # Only switch to 'none' (forces Secure) if front/back ever become cross-site. + COOKIE_SAMESITE: lax + + # Security + BCRYPT_ROUNDS: "10" + SESSION_TIMEOUT_MS: "7200000" + + # Rate Limiting + RATE_LIMIT_TTL: "60" + RATE_LIMIT_MAX: "100" + + # Stripe (Subscriptions & Payments) + STRIPE_SECRET_KEY: "sk_test_51R8p8R4atifoBlu1U9sMJh3rkQbO1G1xeguwFMQYMIMeaLNrTX7YFO5Ovu3P1VfbwcOoEmiy6I0UWi4DThNNzHG100YF75TnJr" + STRIPE_WEBHOOK_SECRET: "whsec_0BLJx3J2LXITCq1cgp9ArzBuMG1W3QMj" + + # Stripe Price IDs (from Stripe Dashboard) + STRIPE_STARTER_MONTHLY_PRICE_ID: "price_1SrIrR4atifoBlu1ZplPEdkD" + STRIPE_STARTER_YEARLY_PRICE_ID: "price_1SrIsm4atifoBlu1ycMAXVGj" + STRIPE_PRO_MONTHLY_PRICE_ID: "price_1SrIs14atifoBlu1BDTlsbK7" + STRIPE_PRO_YEARLY_PRICE_ID: "price_1SrItG4atifoBlu1CiSKold0" + STRIPE_ENTERPRISE_MONTHLY_PRICE_ID: "price_1SrNj94atifoBlu1F6axOXrR" + STRIPE_ENTERPRISE_YEARLY_PRICE_ID: "price_1SrNiA4atifoBlu11RJD0ocG" + + networks: + - xpeditis_internal + - traefik_network + + # Frontend (Next.js) + xpeditis-frontend: + image: rg.fr-par.scw.cloud/weworkstudio/xpeditis-frontend:preprod + healthcheck: + disable: true + deploy: + restart_policy: + condition: on-failure + labels: + - "logging=promtail" + - "logging.service=frontend" + - "traefik.enable=true" + + # Frontend - HTTPS + - "traefik.http.routers.xpeditis-app.rule=Host(`app.preprod.xpeditis.com`) || Host(`www.preprod.xpeditis.com`)" + - "traefik.http.routers.xpeditis-app.entrypoints=websecure" + - "traefik.http.routers.xpeditis-app.tls=true" + - "traefik.http.routers.xpeditis-app.tls.certresolver=letsencrypt" + - "traefik.http.routers.xpeditis-app.priority=50" + - "traefik.http.routers.xpeditis-app.service=xpeditis-app" + - "traefik.http.services.xpeditis-app.loadbalancer.server.port=3000" + - "traefik.http.routers.xpeditis-app.middlewares=xpeditis-app-headers" + + # Frontend Headers + - "traefik.http.middlewares.xpeditis-app-headers.headers.customRequestHeaders.X-Forwarded-Proto=https" + - "traefik.http.middlewares.xpeditis-app-headers.headers.customRequestHeaders.X-Forwarded-For=" + - "traefik.http.middlewares.xpeditis-app-headers.headers.customRequestHeaders.X-Real-IP=" + + # Frontend - HTTP → HTTPS Redirect + - "traefik.http.routers.xpeditis-app-http.rule=Host(`app.preprod.xpeditis.com`) || Host(`www.preprod.xpeditis.com`)" + - "traefik.http.routers.xpeditis-app-http.entrypoints=web" + - "traefik.http.routers.xpeditis-app-http.priority=50" + - "traefik.http.routers.xpeditis-app-http.middlewares=xpeditis-app-redirect" + - "traefik.http.routers.xpeditis-app-http.service=xpeditis-app" + - "traefik.http.middlewares.xpeditis-app-redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.xpeditis-app-redirect.redirectscheme.permanent=true" + environment: + NODE_ENV: production + NEXT_PUBLIC_API_URL: https://api.preprod.xpeditis.com + NEXT_PUBLIC_WS_URL: wss://api.preprod.xpeditis.com + networks: + - traefik_network + + # ─── Logging Stack ───────────────────────────────────────────────────────── + + # Loki - Log aggregation + xpeditis-loki: + image: grafana/loki:3.0.0 + deploy: + restart_policy: + condition: on-failure + volumes: + - xpeditis_loki_data:/loki + configs: + - source: loki_config + target: /etc/loki/local-config.yaml + command: -config.file=/etc/loki/local-config.yaml + healthcheck: + test: ['CMD-SHELL', 'wget --quiet --tries=1 --spider http://localhost:3100/ready || exit 1'] + interval: 15s + timeout: 5s + retries: 5 + networks: + - xpeditis_internal + + # Promtail - Log collector (scrapes Docker container logs) + xpeditis-promtail: + image: grafana/promtail:3.0.0 + deploy: + restart_policy: + condition: on-failure + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + configs: + - source: promtail_config + target: /etc/promtail/config.yml + command: -config.file=/etc/promtail/config.yml + depends_on: + - xpeditis-loki + networks: + - xpeditis_internal + + # Grafana - Log & metrics dashboards + xpeditis-grafana: + image: grafana/grafana:11.0.0 + deploy: + restart_policy: + condition: on-failure + environment: + GF_SECURITY_ADMIN_USER: "David" + GF_SECURITY_ADMIN_PASSWORD: "G9N]71dT80l" + GF_USERS_ALLOW_SIGN_UP: 'false' + GF_AUTH_ANONYMOUS_ENABLED: 'false' + GF_SERVER_ROOT_URL: https://grafana.preprod.xpeditis.com + GF_ANALYTICS_REPORTING_ENABLED: 'false' + GF_ANALYTICS_CHECK_FOR_UPDATES: 'false' + volumes: + - xpeditis_grafana_data:/var/lib/grafana + depends_on: + - xpeditis-loki + networks: + - xpeditis_internal + - traefik_network + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik_network" + + # Grafana - HTTPS + - "traefik.http.routers.xpeditis-grafana.rule=Host(`grafana.preprod.xpeditis.com`)" + - "traefik.http.routers.xpeditis-grafana.entrypoints=websecure" + - "traefik.http.routers.xpeditis-grafana.tls=true" + - "traefik.http.routers.xpeditis-grafana.tls.certresolver=letsencrypt" + - "traefik.http.routers.xpeditis-grafana.priority=50" + - "traefik.http.routers.xpeditis-grafana.service=xpeditis-grafana" + - "traefik.http.services.xpeditis-grafana.loadbalancer.server.port=3000" + - "traefik.http.routers.xpeditis-grafana.middlewares=xpeditis-grafana-headers" + + # Grafana Headers + - "traefik.http.middlewares.xpeditis-grafana-headers.headers.customRequestHeaders.X-Forwarded-Proto=https" + - "traefik.http.middlewares.xpeditis-grafana-headers.headers.customRequestHeaders.X-Forwarded-For=" + - "traefik.http.middlewares.xpeditis-grafana-headers.headers.customRequestHeaders.X-Real-IP=" + + # Grafana - HTTP → HTTPS Redirect + - "traefik.http.routers.xpeditis-grafana-http.rule=Host(`grafana.preprod.xpeditis.com`)" + - "traefik.http.routers.xpeditis-grafana-http.entrypoints=web" + - "traefik.http.routers.xpeditis-grafana-http.priority=50" + - "traefik.http.routers.xpeditis-grafana-http.middlewares=xpeditis-grafana-redirect" + - "traefik.http.routers.xpeditis-grafana-http.service=xpeditis-grafana" + - "traefik.http.middlewares.xpeditis-grafana-redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.xpeditis-grafana-redirect.redirectscheme.permanent=true" + + # Log Exporter - HTTP API to push structured logs to Loki (internal only) + xpeditis-log-exporter: + image: rg.fr-par.scw.cloud/weworkstudio/xpeditis-log-exporter:preprod + deploy: + restart_policy: + condition: on-failure + environment: + PORT: "3200" + LOKI_URL: http://xpeditis-loki:3100 + # LOG_EXPORTER_API_KEY: your-secret-key-here + depends_on: + - xpeditis-loki + networks: + - xpeditis_internal + +volumes: + xpeditis_db_data: + xpeditis_redis_data: + xpeditis_minio_data: + xpeditis_loki_data: + driver: local + xpeditis_grafana_data: + driver: local + +networks: + traefik_network: + external: true + xpeditis_internal: + driver: overlay + internal: true + +configs: + loki_config: + external: true + promtail_config: + external: true