feat: multi-domain nginx configs and TLS scripts for мои-товары.рф / my-products.ru
- nginx/nginx.conf: pre-generated config for both domains (IDN punycode for .рф) - scripts/generate-nginx-conf.sh: generates sites-available config from template per domain - scripts/init-letsencrypt.sh: accepts domain as arg (falls back to .env) - README.md: updated deploy section, removed stale VK_WEIGHT_PRICE_MULTIPLIER, added sync/logs routes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
39
README.md
39
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`
|
- **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
|
- **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
|
- **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.
|
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.
|
||||||
@@ -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}/toggle` | Enable / disable store sync |
|
||||||
| POST | `/catalog/stores/{id}/groups/{gid}/toggle` | Enable / disable group 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)
|
### VK Catalog (requires session)
|
||||||
|
|
||||||
| Method | Path | Description |
|
| 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` | VK Market albums (product groups) |
|
||||||
| GET | `/vk-catalog/albums/{id}/products` | Products in a VK album |
|
| 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
|
### API Docs
|
||||||
|
|
||||||
| Path | Description |
|
| 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 |
|
| `CATALOG_REFRESH_INTERVAL_SECONDS` | `3600` | Sync pipeline interval in seconds |
|
||||||
| `VK_CATEGORY_ID` | `40932` | VK Market category ID for all products |
|
| `VK_CATEGORY_ID` | `40932` | VK Market category ID for all products |
|
||||||
| `VK_STOCK_AMOUNT` | `1000` | Stock amount set for in-sale 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 |
|
||||||
@@ -235,18 +248,28 @@ Flower (queue monitor) at `http://localhost:5555`.
|
|||||||
|
|
||||||
### First deploy (TLS)
|
### First deploy (TLS)
|
||||||
|
|
||||||
```bash
|
Run once per domain (repeat for every domain pointing to this server):
|
||||||
# 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)
|
```bash
|
||||||
sudo ./scripts/init-letsencrypt.sh
|
# 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
|
# 3. Reload nginx
|
||||||
sudo systemctl 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/<domain>.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
|
### Development
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
# Generated from nginx.conf.template — do not edit directly.
|
# 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 <domain>
|
||||||
|
# This file is kept as a reference only; production uses sites-available/*.conf
|
||||||
|
|
||||||
upstream web {
|
upstream web {
|
||||||
server 127.0.0.1:8080;
|
server 127.0.0.1:8080;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── мои-товары.рф ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
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/ {
|
location /.well-known/acme-challenge/ {
|
||||||
root /var/www/certbot;
|
root /var/www/certbot;
|
||||||
@@ -19,10 +23,45 @@ server {
|
|||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
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 /etc/letsencrypt/live/xn--e1afmapc4af.xn--p1af/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/evosync.ru/privkey.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_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
|||||||
51
scripts/generate-nginx-conf.sh
Executable file
51
scripts/generate-nginx-conf.sh
Executable file
@@ -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/<domain>.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 <domain> 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"
|
||||||
@@ -1,32 +1,38 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Obtain TLS certificates from Let's Encrypt.
|
# Obtain a TLS certificate from Let's Encrypt for one domain.
|
||||||
# Run once on first deploy: sudo ./scripts/init-letsencrypt.sh
|
#
|
||||||
# Requires nginx running on the host with acme-challenge location configured.
|
# Usage:
|
||||||
# Set DOMAIN in .env or export it before running:
|
# sudo ./scripts/init-letsencrypt.sh мои-товары.рф
|
||||||
# DOMAIN=example.com sudo -E ./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
|
set -euo pipefail
|
||||||
|
|
||||||
# Load DOMAIN from .env if not already set in environment
|
# ── resolve domain ────────────────────────────────────────────────────────────
|
||||||
if [ -f .env ]; then
|
if [ -n "${1:-}" ]; then
|
||||||
# Extract DOMAIN line, strip quotes and export
|
DOMAIN="$1"
|
||||||
|
else
|
||||||
|
if [ -f .env ]; then
|
||||||
DOMAIN_FROM_ENV=$(grep -E '^DOMAIN=' .env | cut -d= -f2- | tr -d '"'"'" | head -1)
|
DOMAIN_FROM_ENV=$(grep -E '^DOMAIN=' .env | cut -d= -f2- | tr -d '"'"'" | head -1)
|
||||||
DOMAIN="${DOMAIN:-$DOMAIN_FROM_ENV}"
|
DOMAIN="${DOMAIN:-${DOMAIN_FROM_ENV:-}}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "${DOMAIN:-}" ]; then
|
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 <domain> or set DOMAIN= in .env" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
EMAIL="${LETSENCRYPT_EMAIL:-admin@$DOMAIN}"
|
EMAIL="${LETSENCRYPT_EMAIL:-admin@$DOMAIN}"
|
||||||
CERTBOT_DIR="./certbot"
|
|
||||||
ACME_DIR="/var/www/certbot"
|
ACME_DIR="/var/www/certbot"
|
||||||
|
|
||||||
echo "==> Creating certbot directories..."
|
echo "==> Obtaining certificate for: $DOMAIN (www.$DOMAIN)"
|
||||||
mkdir -p "$CERTBOT_DIR/conf" "$CERTBOT_DIR/www"
|
echo " Email: $EMAIL"
|
||||||
|
|
||||||
echo "==> Ensuring acme-challenge directory exists on host..."
|
echo "==> Ensuring acme-challenge directory exists..."
|
||||||
sudo mkdir -p "$ACME_DIR"
|
sudo mkdir -p "$ACME_DIR"
|
||||||
sudo chmod 755 "$ACME_DIR"
|
sudo chmod 755 "$ACME_DIR"
|
||||||
|
|
||||||
@@ -40,23 +46,14 @@ sudo certbot certonly \
|
|||||||
-d "$DOMAIN" \
|
-d "$DOMAIN" \
|
||||||
-d "www.$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 ""
|
||||||
echo "Regenerate nginx config from template:"
|
echo "==> Certificate obtained for $DOMAIN"
|
||||||
echo " DOMAIN=$DOMAIN envsubst '\$DOMAIN' < nginx/nginx.conf.template > nginx/nginx.conf"
|
echo " /etc/letsencrypt/live/$DOMAIN/fullchain.pem"
|
||||||
|
echo " /etc/letsencrypt/live/$DOMAIN/privkey.pem"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Certificate files:"
|
echo "==> Generate nginx config and reload:"
|
||||||
echo " - $CERTBOT_DIR/conf/fullchain.pem"
|
echo " sudo ./scripts/generate-nginx-conf.sh $DOMAIN"
|
||||||
echo " - $CERTBOT_DIR/conf/privkey.pem"
|
echo " sudo nginx -t && sudo systemctl reload nginx"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Configure nginx:"
|
echo "==> Auto-renewal (add to /etc/cron.d/certbot if not already present):"
|
||||||
echo " ssl_certificate $CERTBOT_DIR/conf/fullchain.pem;"
|
echo " 0 3 * * * root certbot renew --quiet && systemctl reload nginx"
|
||||||
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"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user