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:
@@ -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=
|
||||||
|
|||||||
59
README.md
59
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) |
|
| `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
|
||||||
|
|||||||
@@ -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:-}
|
||||||
|
|||||||
@@ -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
36
nginx/nginx.conf.template
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user