From 4f4081c54ca667c6b99b3e03a8468a348e8d1d8a Mon Sep 17 00:00:00 2001 From: mguschin Date: Tue, 12 May 2026 14:01:38 +0300 Subject: [PATCH] config: make domain configurable via DOMAIN env var Replace hardcoded evosync.ru with a DOMAIN variable read from .env. nginx.conf is now generated from nginx.conf.template via envsubst; init-letsencrypt.sh reads DOMAIN from .env and fails loudly if unset. README documents the new variable and first-deploy TLS workflow. Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 3 +- README.md | 59 ++++++++++++++++++++++++++++++------- docker-compose.yml | 2 +- nginx/nginx.conf | 2 ++ nginx/nginx.conf.template | 36 ++++++++++++++++++++++ scripts/init-letsencrypt.sh | 24 ++++++++++++--- 6 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 nginx/nginx.conf.template diff --git a/.env.example b/.env.example index 5535e9c..ff19ff2 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,8 @@ DB_PASSWORD=evosync # App SECRET_KEY=change-me-in-production -BASE_URL=https://evosync.ru +DOMAIN=yourdomain.com +BASE_URL=https://${DOMAIN} # Evotor EVOTOR_APP_ID= diff --git a/README.md b/README.md index 078a59f..c168ea0 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Web service for syncing a product catalog from Evotor POS → VK Market. Users c | `sync_filters` | Store / group inclusion filters (entity_type: store / group) | | `cached_stores` | Cached list of Evotor stores | | `cached_groups` | Cached Evotor product groups | -| `cached_products` | Cached Evotor product catalog | +| `cached_products` | Cached Evotor products; `vk_product_id` stores the VK market item ID after first push | | `vk_cached_albums` | Cached VK Market albums (product groups) | | `vk_cached_products` | Cached VK Market products | | `roles` | RBAC roles | @@ -69,20 +69,40 @@ Web service for syncing a product catalog from Evotor POS → VK Market. Users c Periodic tasks run via **Celery Beat** and are executed by the **worker** service. -| Task | Schedule | Description | -|------|----------|-------------| -| `web.tasks.catalog.refresh_catalog` | Every `CATALOG_REFRESH_INTERVAL_SECONDS` | Fetches stores, product groups, and products from the Evotor API for every connected user; upserts into `cached_stores`, `cached_groups`, `cached_products` | -| `web.tasks.vk_catalog.refresh_vk_catalog` | Every `CATALOG_REFRESH_INTERVAL_SECONDS` | Fetches Market albums and products from VK API for every connected user; upserts into `vk_cached_albums`, `vk_cached_products` | +Beat fires a single **sync pipeline** every `CATALOG_REFRESH_INTERVAL_SECONDS`. The three tasks run as a Celery chain — each step starts only after the previous one completes: -**Evotor sync sequence per user:** +``` +run_sync_pipeline (beat entry) + └─► refresh_catalog — fetch Evotor stores / groups / products + └─► refresh_vk_catalog — fetch VK Market albums / products + └─► mirror_to_vk — push Evotor → VK +``` + +| Task | Queue | Description | +|------|-------|-------------| +| `web.tasks.celery_app.run_sync_pipeline` | default | Beat entry point; dispatches the chain | +| `web.tasks.catalog.refresh_catalog` | default | Fetches Evotor catalog for all connected users; upserts `cached_stores`, `cached_groups`, `cached_products` | +| `web.tasks.vk_catalog.refresh_vk_catalog` | default | Fetches VK Market albums and products for all connected users; upserts `vk_cached_albums`, `vk_cached_products` | +| `web.tasks.vk_sync.mirror_to_vk` | sync | Mirrors enabled Evotor groups/products → VK Market (create or conditional update) | + +**Evotor fetch sequence per user:** 1. `GET /stores` → upsert `cached_stores` 2. For each store: `GET /stores/{id}/product-groups` → upsert `cached_groups` 3. For each store: `GET /stores/{id}/products` → upsert `cached_products` -**VK sync sequence per user:** +**VK fetch sequence per user:** 1. `market.getAlbums` → upsert `vk_cached_albums` 2. `market.get` (extended=1, paginated) → upsert `vk_cached_products` with album membership +**Mirror logic per user (`mirror_to_vk`):** +- Skips stores not in enabled store filters; skips groups not in enabled group filters +- For each enabled group: ensures a matching VK album exists (creates via `market.addAlbum` if missing) +- For each product in the group: + - **Create** (`allow_to_sell=true`, no `vk_product_id` yet): uploads default photo once per run, calls `market.add`, assigns to album, stores returned `vk_product_id` + - **Update** (has `vk_product_id`): calls `market.edit` only if name, price, description, or stock_amount changed vs the cached VK state + - **Skip**: product disabled and never pushed, or nothing changed +- Price for weight measures (`г`, `гр`, etc.) is multiplied by `VK_WEIGHT_PRICE_MULTIPLIER` before conversion to kopecks + Per-user failures are logged and skipped — one broken token does not block other users. Evotor stores that return `402 Payment Required` (subscription limit) are silently skipped at debug log level. @@ -185,13 +205,17 @@ All settings are read from environment variables or a `.env` file: | `DATABASE_URL` | `mysql+pymysql://…@db:3306/evosync` | MariaDB connection string | | `REDIS_URL` | `redis://redis:6379/0` | Redis connection string | | `SECRET_KEY` | `change-me-in-production` | Session signing key | -| `BASE_URL` | `http://localhost:8000` | Public URL of the service | +| `DOMAIN` | — | Public domain name (e.g. `example.com`); used to derive `BASE_URL` and nginx config | +| `BASE_URL` | `https://${DOMAIN}` | Public URL of the service | | `EVOTOR_APP_ID` | — | Evotor application ID | | `EVOTOR_WEBHOOK_SECRET` | — | Bearer secret for webhook endpoints | | `JIVOSITE_WIDGET_ID` | — | JivoSite widget ID | | `VK_DEFAULT_PHOTO_PATH` | `/app/default_product.png` | Fallback image path for VK products | | `VK_API_VERSION` | `5.199` | VK API version | -| `CATALOG_REFRESH_INTERVAL_SECONDS` | `3600` | Evotor + VK catalog sync interval (s) | +| `CATALOG_REFRESH_INTERVAL_SECONDS` | `3600` | Sync pipeline interval in seconds | +| `VK_CATEGORY_ID` | `40932` | VK Market category ID for all products | +| `VK_STOCK_AMOUNT` | `1000` | Stock amount set for in-sale products | +| `VK_WEIGHT_PRICE_MULTIPLIER` | `10` | Price multiplier for weight-unit products (г, гр, …) | | `INVITE_EXPIRE_HOURS` | `48` | Invite link TTL in hours | | `EMAIL_PROVIDER` | `console` | Email provider (console / smtp / …) | | `SMS_PROVIDER` | `console` | SMS provider | @@ -202,13 +226,28 @@ All settings are read from environment variables or a `.env` file: ## Running ```bash -cp .env.example .env # fill in your values +cp .env.example .env # set DOMAIN and other values docker compose up -d --build ``` App is available at `http://localhost:8080`. Flower (queue monitor) at `http://localhost:5555`. +### First deploy (TLS) + +```bash +# 1. Generate nginx config from template +DOMAIN=yourdomain.com envsubst '${DOMAIN}' < nginx/nginx.conf.template > nginx/nginx.conf + +# 2. Obtain TLS certificate (reads DOMAIN from .env automatically) +sudo ./scripts/init-letsencrypt.sh + +# 3. Reload nginx +sudo systemctl reload nginx +``` + +To change the domain later, repeat all three steps with the new domain. + ### Development ```bash diff --git a/docker-compose.yml b/docker-compose.yml index 48dc7cf..91bd0b0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,7 +39,7 @@ services: DATABASE_URL: mysql+pymysql://${DB_USER}:${DB_PASSWORD}@db:3306/${DB_NAME} REDIS_URL: redis://redis:6379/0 SECRET_KEY: ${SECRET_KEY:-change-me-in-production} - BASE_URL: ${BASE_URL:-https://evosync.ru} + BASE_URL: ${BASE_URL:-https://${DOMAIN}} EVOTOR_APP_ID: ${EVOTOR_APP_ID:-} EVOTOR_WEBHOOK_SECRET: ${EVOTOR_WEBHOOK_SECRET:-} JIVOSITE_WIDGET_ID: ${JIVOSITE_WIDGET_ID:-} diff --git a/nginx/nginx.conf b/nginx/nginx.conf index bf1c8ba..a4c38ba 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,3 +1,5 @@ +# Generated from nginx.conf.template — do not edit directly. +# Regenerate: DOMAIN=yourdomain.com envsubst '${DOMAIN}' < nginx/nginx.conf.template > nginx/nginx.conf upstream web { server 127.0.0.1:8080; } diff --git a/nginx/nginx.conf.template b/nginx/nginx.conf.template new file mode 100644 index 0000000..fcb9985 --- /dev/null +++ b/nginx/nginx.conf.template @@ -0,0 +1,36 @@ +upstream web { + server 127.0.0.1:8080; +} + +server { + listen 80; + server_name ${DOMAIN} www.${DOMAIN}; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + server_name ${DOMAIN} www.${DOMAIN}; + + ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + location / { + proxy_pass http://web; + 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; + } +} diff --git a/scripts/init-letsencrypt.sh b/scripts/init-letsencrypt.sh index 7acffd8..809bc5c 100755 --- a/scripts/init-letsencrypt.sh +++ b/scripts/init-letsencrypt.sh @@ -1,12 +1,25 @@ #!/bin/bash -# Obtain TLS certificates from Let's Encrypt for evosync.ru +# Obtain TLS certificates from Let's Encrypt. # Run once on first deploy: sudo ./scripts/init-letsencrypt.sh -# Requires nginx running on the host with acme-challenge location configured +# Requires nginx running on the host with acme-challenge location configured. +# Set DOMAIN in .env or export it before running: +# DOMAIN=example.com sudo -E ./scripts/init-letsencrypt.sh set -euo pipefail -DOMAIN="evosync.ru" -EMAIL="${LETSENCRYPT_EMAIL:-admin@evosync.ru}" +# Load DOMAIN from .env if not already set in environment +if [ -f .env ]; then + # Extract DOMAIN line, strip quotes and export + DOMAIN_FROM_ENV=$(grep -E '^DOMAIN=' .env | cut -d= -f2- | tr -d '"'"'" | head -1) + DOMAIN="${DOMAIN:-$DOMAIN_FROM_ENV}" +fi + +if [ -z "${DOMAIN:-}" ]; then + echo "ERROR: DOMAIN is not set. Add DOMAIN=yourdomain.com to .env or export it." >&2 + exit 1 +fi + +EMAIL="${LETSENCRYPT_EMAIL:-admin@$DOMAIN}" CERTBOT_DIR="./certbot" ACME_DIR="/var/www/certbot" @@ -34,6 +47,9 @@ sudo chown "$(whoami):$(whoami)" "$CERTBOT_DIR/conf"/*.pem echo "==> Done! TLS certificate installed for $DOMAIN" echo "" +echo "Regenerate nginx config from template:" +echo " DOMAIN=$DOMAIN envsubst '\$DOMAIN' < nginx/nginx.conf.template > nginx/nginx.conf" +echo "" echo "Certificate files:" echo " - $CERTBOT_DIR/conf/fullchain.pem" echo " - $CERTBOT_DIR/conf/privkey.pem"