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 <noreply@anthropic.com>
This commit is contained in:
mguschin
2026-05-12 14:01:38 +03:00
parent 796cf49ff9
commit 4f4081c54c
6 changed files with 110 additions and 16 deletions

View File

@@ -6,7 +6,8 @@ DB_PASSWORD=evosync
# App # App
SECRET_KEY=change-me-in-production SECRET_KEY=change-me-in-production
BASE_URL=https://evosync.ru DOMAIN=yourdomain.com
BASE_URL=https://${DOMAIN}
# Evotor # Evotor
EVOTOR_APP_ID= EVOTOR_APP_ID=

View File

@@ -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) | | `sync_filters` | Store / group inclusion filters (entity_type: store / group) |
| `cached_stores` | Cached list of Evotor stores | | `cached_stores` | Cached list of Evotor stores |
| `cached_groups` | Cached Evotor product groups | | `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_albums` | Cached VK Market albums (product groups) |
| `vk_cached_products` | Cached VK Market products | | `vk_cached_products` | Cached VK Market products |
| `roles` | RBAC roles | | `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. Periodic tasks run via **Celery Beat** and are executed by the **worker** service.
| Task | Schedule | Description | 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:
|------|----------|-------------|
| `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` |
**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` 1. `GET /stores` → upsert `cached_stores`
2. For each store: `GET /stores/{id}/product-groups` → upsert `cached_groups` 2. For each store: `GET /stores/{id}/product-groups` → upsert `cached_groups`
3. For each store: `GET /stores/{id}/products` → upsert `cached_products` 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` 1. `market.getAlbums` → upsert `vk_cached_albums`
2. `market.get` (extended=1, paginated) → upsert `vk_cached_products` with album membership 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. 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. 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 | | `DATABASE_URL` | `mysql+pymysql://…@db:3306/evosync` | MariaDB connection string |
| `REDIS_URL` | `redis://redis:6379/0` | Redis connection string | | `REDIS_URL` | `redis://redis:6379/0` | Redis connection string |
| `SECRET_KEY` | `change-me-in-production` | Session signing key | | `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_APP_ID` | — | Evotor application ID |
| `EVOTOR_WEBHOOK_SECRET` | — | Bearer secret for webhook endpoints | | `EVOTOR_WEBHOOK_SECRET` | — | Bearer secret for webhook endpoints |
| `JIVOSITE_WIDGET_ID` | — | JivoSite widget ID | | `JIVOSITE_WIDGET_ID` | — | JivoSite widget ID |
| `VK_DEFAULT_PHOTO_PATH` | `/app/default_product.png` | Fallback image path for VK products | | `VK_DEFAULT_PHOTO_PATH` | `/app/default_product.png` | Fallback image path for VK products |
| `VK_API_VERSION` | `5.199` | VK API version | | `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 | | `INVITE_EXPIRE_HOURS` | `48` | Invite link TTL in hours |
| `EMAIL_PROVIDER` | `console` | Email provider (console / smtp / …) | | `EMAIL_PROVIDER` | `console` | Email provider (console / smtp / …) |
| `SMS_PROVIDER` | `console` | SMS provider | | `SMS_PROVIDER` | `console` | SMS provider |
@@ -202,13 +226,28 @@ All settings are read from environment variables or a `.env` file:
## Running ## Running
```bash ```bash
cp .env.example .env # fill in your values cp .env.example .env # set DOMAIN and other values
docker compose up -d --build docker compose up -d --build
``` ```
App is available at `http://localhost:8080`. App is available at `http://localhost:8080`.
Flower (queue monitor) at `http://localhost:5555`. 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 ### Development
```bash ```bash

View File

@@ -39,7 +39,7 @@ services:
DATABASE_URL: mysql+pymysql://${DB_USER}:${DB_PASSWORD}@db:3306/${DB_NAME} DATABASE_URL: mysql+pymysql://${DB_USER}:${DB_PASSWORD}@db:3306/${DB_NAME}
REDIS_URL: redis://redis:6379/0 REDIS_URL: redis://redis:6379/0
SECRET_KEY: ${SECRET_KEY:-change-me-in-production} 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_APP_ID: ${EVOTOR_APP_ID:-}
EVOTOR_WEBHOOK_SECRET: ${EVOTOR_WEBHOOK_SECRET:-} EVOTOR_WEBHOOK_SECRET: ${EVOTOR_WEBHOOK_SECRET:-}
JIVOSITE_WIDGET_ID: ${JIVOSITE_WIDGET_ID:-} JIVOSITE_WIDGET_ID: ${JIVOSITE_WIDGET_ID:-}

View File

@@ -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 { upstream web {
server 127.0.0.1:8080; server 127.0.0.1:8080;
} }

36
nginx/nginx.conf.template Normal file
View File

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

View File

@@ -1,12 +1,25 @@
#!/bin/bash #!/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 # 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 set -euo pipefail
DOMAIN="evosync.ru" # Load DOMAIN from .env if not already set in environment
EMAIL="${LETSENCRYPT_EMAIL:-admin@evosync.ru}" 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" CERTBOT_DIR="./certbot"
ACME_DIR="/var/www/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 "==> Done! TLS certificate installed for $DOMAIN"
echo "" echo ""
echo "Regenerate nginx config from template:"
echo " DOMAIN=$DOMAIN envsubst '\$DOMAIN' < nginx/nginx.conf.template > nginx/nginx.conf"
echo ""
echo "Certificate files:" echo "Certificate files:"
echo " - $CERTBOT_DIR/conf/fullchain.pem" echo " - $CERTBOT_DIR/conf/fullchain.pem"
echo " - $CERTBOT_DIR/conf/privkey.pem" echo " - $CERTBOT_DIR/conf/privkey.pem"