diff --git a/README.md b/README.md index c168ea0..e500b81 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,8 @@ run_sync_pipeline (beat entry) - **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 +- Price is multiplied by the per-user `price_multiplier` from `sync_configs` (configurable on the `/sync` page) +- Description is built as `"Name (цена за N unit.)"` when the product has a `measure_name` 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. @@ -180,6 +181,13 @@ Evotor stores that return `402 Payment Required` (subscription limit) are silent | POST | `/catalog/stores/{id}/toggle` | Enable / disable store sync | | POST | `/catalog/stores/{id}/groups/{gid}/toggle` | Enable / disable group sync | +### Sync Settings (requires session) + +| Method | Path | Description | +|----------|------------------|------------------------------------------------------| +| GET | `/sync` | Sync settings: task on/off switches, price multiplier | +| POST | `/sync/settings` | Save sync settings | + ### VK Catalog (requires session) | Method | Path | Description | @@ -187,6 +195,12 @@ Evotor stores that return `402 Payment Required` (subscription limit) are silent | GET | `/vk-catalog/albums` | VK Market albums (product groups) | | GET | `/vk-catalog/albums/{id}/products` | Products in a VK album | +### Admin Logs (`/admin`, roles: admin / system) + +| Method | Path | Description | +|--------|---------------|-------------------------------------------------------| +| GET | `/admin/logs` | API request/response log viewer with filters and pagination | + ### API Docs | Path | Description | @@ -215,7 +229,6 @@ All settings are read from environment variables or a `.env` file: | `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 | @@ -235,18 +248,28 @@ 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 +Run once per domain (repeat for every domain pointing to this server): -# 2. Obtain TLS certificate (reads DOMAIN from .env automatically) -sudo ./scripts/init-letsencrypt.sh +```bash +# 1. Obtain TLS certificate +sudo ./scripts/init-letsencrypt.sh my-products.ru +sudo ./scripts/init-letsencrypt.sh мои-товары.рф + +# 2. Generate nginx site config and symlink it into sites-enabled +sudo ./scripts/generate-nginx-conf.sh my-products.ru +sudo ./scripts/generate-nginx-conf.sh мои-товары.рф # 3. Reload nginx sudo systemctl reload nginx ``` -To change the domain later, repeat all three steps with the new domain. +`generate-nginx-conf.sh` expands `nginx/nginx.conf.template` with the given domain and writes the result to `/etc/nginx/sites-available/.conf`, then creates a symlink in `sites-enabled`. + +Set up auto-renewal (if not already configured by certbot): + +``` +0 3 * * * root certbot renew --quiet && systemctl reload nginx +``` ### Development diff --git a/nginx/nginx.conf b/nginx/nginx.conf index a4c38ba..8cfb24c 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,12 +1,16 @@ # Generated from nginx.conf.template — do not edit directly. -# Regenerate: DOMAIN=yourdomain.com envsubst '${DOMAIN}' < nginx/nginx.conf.template > nginx/nginx.conf +# Regenerate per domain: sudo ./scripts/generate-nginx-conf.sh +# This file is kept as a reference only; production uses sites-available/*.conf + upstream web { server 127.0.0.1:8080; } +# ── мои-товары.рф ───────────────────────────────────────────────────────────── + server { listen 80; - server_name evosync.ru www.evosync.ru; + server_name xn--e1afmapc4af.xn--p1af www.xn--e1afmapc4af.xn--p1af; location /.well-known/acme-challenge/ { root /var/www/certbot; @@ -19,10 +23,45 @@ server { server { listen 443 ssl; - server_name evosync.ru www.evosync.ru; + server_name xn--e1afmapc4af.xn--p1af www.xn--e1afmapc4af.xn--p1af; - ssl_certificate /etc/letsencrypt/live/evosync.ru/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/evosync.ru/privkey.pem; + ssl_certificate /etc/letsencrypt/live/xn--e1afmapc4af.xn--p1af/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/xn--e1afmapc4af.xn--p1af/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; + } +} + +# ── my-products.ru ──────────────────────────────────────────────────────────── + +server { + listen 80; + server_name my-products.ru www.my-products.ru; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + server_name my-products.ru www.my-products.ru; + + ssl_certificate /etc/letsencrypt/live/my-products.ru/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/my-products.ru/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; diff --git a/scripts/generate-nginx-conf.sh b/scripts/generate-nginx-conf.sh new file mode 100755 index 0000000..a747693 --- /dev/null +++ b/scripts/generate-nginx-conf.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Generate an nginx site config for one domain from the template. +# +# Usage: +# sudo ./scripts/generate-nginx-conf.sh мои-товары.рф +# sudo ./scripts/generate-nginx-conf.sh my-products.ru +# +# Writes to /etc/nginx/sites-available/.conf and symlinks to sites-enabled. +# If no argument is given, DOMAIN is read from .env. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_DIR="$(dirname "$SCRIPT_DIR")" +TEMPLATE="$REPO_DIR/nginx/nginx.conf.template" + +# ── resolve domain ──────────────────────────────────────────────────────────── +if [ -n "${1:-}" ]; then + DOMAIN="$1" +else + if [ -f "$REPO_DIR/.env" ]; then + DOMAIN_FROM_ENV=$(grep -E '^DOMAIN=' "$REPO_DIR/.env" | cut -d= -f2- | tr -d '"'"'" | head -1) + DOMAIN="${DOMAIN:-${DOMAIN_FROM_ENV:-}}" + fi +fi + +if [ -z "${DOMAIN:-}" ]; then + echo "ERROR: no domain specified." >&2 + echo "Usage: $0 or set DOMAIN= in .env" >&2 + exit 1 +fi + +CONF_FILE="/etc/nginx/sites-available/${DOMAIN}.conf" +ENABLED_LINK="/etc/nginx/sites-enabled/${DOMAIN}.conf" + +echo "==> Generating nginx config for: $DOMAIN" +DOMAIN="$DOMAIN" envsubst '$DOMAIN' < "$TEMPLATE" | sudo tee "$CONF_FILE" > /dev/null + +if [ ! -L "$ENABLED_LINK" ]; then + sudo ln -s "$CONF_FILE" "$ENABLED_LINK" + echo "==> Symlinked to sites-enabled" +else + echo "==> Symlink already exists in sites-enabled" +fi + +echo "==> Testing nginx config..." +sudo nginx -t + +echo "" +echo "==> Config written to: $CONF_FILE" +echo " Reload nginx to apply: sudo systemctl reload nginx" diff --git a/scripts/init-letsencrypt.sh b/scripts/init-letsencrypt.sh index 809bc5c..87b1b0a 100755 --- a/scripts/init-letsencrypt.sh +++ b/scripts/init-letsencrypt.sh @@ -1,32 +1,38 @@ #!/bin/bash -# 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. -# Set DOMAIN in .env or export it before running: -# DOMAIN=example.com sudo -E ./scripts/init-letsencrypt.sh +# Obtain a TLS certificate from Let's Encrypt for one domain. +# +# Usage: +# sudo ./scripts/init-letsencrypt.sh мои-товары.рф +# sudo ./scripts/init-letsencrypt.sh my-products.ru +# +# If no argument is given, DOMAIN is read from .env. +# Run once per domain on first deploy. set -euo pipefail -# 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}" +# ── resolve domain ──────────────────────────────────────────────────────────── +if [ -n "${1:-}" ]; then + DOMAIN="$1" +else + if [ -f .env ]; then + DOMAIN_FROM_ENV=$(grep -E '^DOMAIN=' .env | cut -d= -f2- | tr -d '"'"'" | head -1) + DOMAIN="${DOMAIN:-${DOMAIN_FROM_ENV:-}}" + fi fi if [ -z "${DOMAIN:-}" ]; then - echo "ERROR: DOMAIN is not set. Add DOMAIN=yourdomain.com to .env or export it." >&2 + echo "ERROR: no domain specified." >&2 + echo "Usage: $0 or set DOMAIN= in .env" >&2 exit 1 fi EMAIL="${LETSENCRYPT_EMAIL:-admin@$DOMAIN}" -CERTBOT_DIR="./certbot" ACME_DIR="/var/www/certbot" -echo "==> Creating certbot directories..." -mkdir -p "$CERTBOT_DIR/conf" "$CERTBOT_DIR/www" +echo "==> Obtaining certificate for: $DOMAIN (www.$DOMAIN)" +echo " Email: $EMAIL" -echo "==> Ensuring acme-challenge directory exists on host..." +echo "==> Ensuring acme-challenge directory exists..." sudo mkdir -p "$ACME_DIR" sudo chmod 755 "$ACME_DIR" @@ -40,23 +46,14 @@ sudo certbot certonly \ -d "$DOMAIN" \ -d "www.$DOMAIN" -echo "==> Copying certificates to project directory..." -sudo cp "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" "$CERTBOT_DIR/conf/fullchain.pem" -sudo cp "/etc/letsencrypt/live/$DOMAIN/privkey.pem" "$CERTBOT_DIR/conf/privkey.pem" -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 "==> Certificate obtained for $DOMAIN" +echo " /etc/letsencrypt/live/$DOMAIN/fullchain.pem" +echo " /etc/letsencrypt/live/$DOMAIN/privkey.pem" echo "" -echo "Certificate files:" -echo " - $CERTBOT_DIR/conf/fullchain.pem" -echo " - $CERTBOT_DIR/conf/privkey.pem" +echo "==> Generate nginx config and reload:" +echo " sudo ./scripts/generate-nginx-conf.sh $DOMAIN" +echo " sudo nginx -t && sudo systemctl reload nginx" echo "" -echo "Configure nginx:" -echo " ssl_certificate $CERTBOT_DIR/conf/fullchain.pem;" -echo " ssl_certificate_key $CERTBOT_DIR/conf/privkey.pem;" -echo "" -echo "Set up auto-renewal with: sudo crontab -e" -echo "Add: 0 3 * * * certbot renew --quiet && systemctl reload nginx" +echo "==> Auto-renewal (add to /etc/cron.d/certbot if not already present):" +echo " 0 3 * * * root certbot renew --quiet && systemctl reload nginx"