diff --git a/.env.example b/.env.example index c006720..be0c81b 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,7 @@ COMMISSION_ENABLED=true COMMISSION_PERCENT=5 # --- Commission Wallets --- +# Начальные адреса комиссионных кошельков. Редактируются в админке (Кошельки → Edit Wallets), значения в БД имеют приоритет. COMMISSION_WALLET_BTC= COMMISSION_WALLET_LTC= COMMISSION_WALLET_USDT= @@ -70,3 +71,7 @@ DEFAULT_LANGUAGE=en # OpenAI-совместимый endpoint и ключ (используются, если не заданы в site_settings) CHATBOT_API_ENDPOINT=https://api.openai.com/v1 CHATBOT_API_KEY= + +# --- Deploy --- +# Версия образов из Gitea Container Registry (по умолчанию latest) +IMAGE_TAG=latest diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 891bb6a..1a51730 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -1,70 +1,96 @@ -name: "Release: ARM64 Docker Image" +name: "Release: multi-arch Docker images" on: push: tags: - "v*" + workflow_dispatch: + inputs: + version: + description: "Version tag (without v prefix)" + required: false + default: "" jobs: - build-arm64: - # Нативный ARM64 runner на Orange Pi Zero 2 (aarch64). - # GitHub недоступен в этой сети — НЕ используем actions/*, только чистые run: шаги. - # node:22-alpine не содержит bash → используем sh - runs-on: arm64 + build-push: + # Buildx runner на x86_64 хосте (docker:27.5-cli + host docker.sock). + # Собирает linux/amd64 + linux/arm64 одним buildx build (qemu из коробки) + # и пушит оба образа (бот + Next.js админка) в Gitea Container Registry. + runs-on: buildx defaults: run: shell: sh env: + REGISTRY: git.softuniq.eu REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + VERSION: "" steps: - name: Checkout code run: | set -e - apk add --no-cache git >/dev/null 2>&1 || true + apk add --no-cache git curl >/dev/null 2>&1 || true + if [ -n "${GITHUB_REF_NAME}" ] && [ "${GITHUB_REF_NAME}" != "main" ]; then + # Запуск по тегу: clone конкретного тега + REF=${GITHUB_REF_NAME} + elif [ -n "${{ github.event.inputs.version }}" ]; then + REF=v${{ github.event.inputs.version }} + else + REF=${GITHUB_REF_NAME} + fi + echo "REF=${REF}" cd /tmp rm -rf release-src - git clone --depth 1 --branch "${GITHUB_REF_NAME}" \ - "https://oauth2:${REGISTRY_TOKEN}@git.softuniq.eu/Telegram-Market/telegram-shop.git" release-src + git clone --depth 1 --branch "${REF}" \ + "https://oauth2:${REGISTRY_TOKEN}@${REGISTRY}/Telegram-Market/telegram-shop.git" release-src cd release-src git rev-parse --short HEAD - name: Extract version run: | - VERSION=${GITHUB_REF_NAME#v} - echo "VERSION=${VERSION}" >> "$GITHUB_ENV" - echo "Build version: ${VERSION}" + if [ -n "${{ github.event.inputs.version }}" ]; then + V=${{ github.event.inputs.version }} + else + V=${GITHUB_REF_NAME#v} + fi + echo "VERSION=${V}" >> "$GITHUB_ENV" + echo "Build version: ${V}" - - name: Build ARM64 image + - name: Login to Gitea Container Registry + run: | + apk add --no-cache docker-cli >/dev/null 2>&1 || true + echo "${REGISTRY_TOKEN}" | docker login ${REGISTRY} -u oauth2 --password-stdin + + - name: Build and push bot image (amd64 + arm64) working-directory: /tmp/release-src run: | set -e - # docker-cli не входит в node:22-alpine — ставим через apk - apk add --no-cache docker-cli >/dev/null 2>&1 - docker build \ + docker buildx build \ + --platform linux/amd64,linux/arm64 \ --file ./Dockerfile \ - --tag git.softuniq.eu/telegram-market/telegram-shop:${VERSION} \ - --tag git.softuniq.eu/telegram-market/telegram-shop:latest \ + --tag ${REGISTRY}/telegram-market/telegram-shop:${VERSION} \ + --tag ${REGISTRY}/telegram-market/telegram-shop:latest \ + --push \ . - - name: Push to Gitea Container Registry (best effort) + - name: Build and push admin image (amd64 + arm64) working-directory: /tmp/release-src run: | set -e - apk add --no-cache docker-cli >/dev/null 2>&1 || true - echo "${REGISTRY_TOKEN}" | docker login git.softuniq.eu -u oauth2 --password-stdin \ - || { echo "registry login failed, skipping push"; exit 0; } - docker push git.softuniq.eu/telegram-market/telegram-shop:${VERSION} \ - || echo "push tag failed (may need package scope)" - docker push git.softuniq.eu/telegram-market/telegram-shop:latest \ - || echo "push latest failed (may need package scope)" - docker logout git.softuniq.eu || true + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --file ./admin-next/Dockerfile \ + --tag ${REGISTRY}/telegram-market/telegram-shop-admin:${VERSION} \ + --tag ${REGISTRY}/telegram-market/telegram-shop-admin:latest \ + --push \ + ./admin-next - - name: Save image tarball artifact - working-directory: /tmp/release-src + - name: Verify pushed manifests run: | set -e apk add --no-cache docker-cli >/dev/null 2>&1 || true - mkdir -p /tmp/release-artifacts - docker save git.softuniq.eu/telegram-market/telegram-shop:${VERSION} \ - -o /tmp/release-artifacts/telegram-shop-arm64-${VERSION}.tar - ls -lah /tmp/release-artifacts/ + echo "${REGISTRY_TOKEN}" | docker login ${REGISTRY} -u oauth2 --password-stdin >/dev/null 2>&1 || true + docker buildx imagetools inspect ${REGISTRY}/telegram-market/telegram-shop:${VERSION} \ + | grep -E "Platform|Digest" | head -4 + docker buildx imagetools inspect ${REGISTRY}/telegram-market/telegram-shop-admin:${VERSION} \ + | grep -E "Platform|Digest" | head -4 + docker logout ${REGISTRY} || true diff --git a/README.md b/README.md index bebd173..7108ae2 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,14 @@ bash install.sh 1. Определит архитектуру (x86_64 / ARM64 / ARMv7) 2. Установит Docker если не установлен 3. Создаст `.env` из шаблона -4. Проверит обязательные переменные -5. Соберёт Docker-образ под текущую архитектуру -6. Запустит контейнер и проверит health-check +4. Интерактивно запросит недостающие переменные (BOT_TOKEN, ADMIN_IDS) и сгенерирует ключи (ENCRYPTION_KEY, ADMIN_SECRET, SUPER_ADMIN_SECRET) если они не заданы +5. Установит SHOP_ACTIVATED=false для нового магазина (стартует заблокированным) +6. Автоматически определит LAN IP и установит ADMIN_URL +7. Соберёт Docker-образ под текущую архитектуру +8. Запустит контейнер и проверит health-check + +#### Активация магазина +После установки магазин стартует заблокированным (SHOP_ACTIVATED=false). Оператор получает onion-адрес от клиента, заходит в админку по onion/LAN, входит с SUPER_ADMIN_SECRET, добавляет комиссионные кошельки (Кошельки → Edit Wallets), настраивает бота, нажимает "Activate Shop" в Настройках. ### Ручная установка @@ -46,8 +51,8 @@ git clone && cd telegram-shop cp .env.example .env nano .env # заполнить BOT_TOKEN, ADMIN_IDS, ENCRYPTION_KEY -# 3. Собрать и запустить -docker compose up -d --build +# 3. Запустить (образы тянутся из Gitea Container Registry, сборка не нужна) +docker compose up -d # 4. Проверить статус docker compose ps @@ -55,6 +60,8 @@ curl http://localhost:3001/health # бот (health-сервер) curl -o /dev/null -w "%{http_code}\n" http://localhost:3000/login # новая админка ``` +> Образы бота и админки собираются в CI (Gitea Actions, buildx multi-arch) и публикуются в Gitea Container Registry. На сервере ничего не компилируется — только `docker compose pull`. Для пиннинга версии: `IMAGE_TAG=1.2.9 docker compose up -d`. Локальная сборка (dev): `docker compose up -d --build`. + ## Настройка .env Скопируйте `.env.example` в `.env` и заполните: @@ -82,6 +89,7 @@ curl -o /dev/null -w "%{http_code}\n" http://localhost:3000/login # новая | `WG_ENDPOINT` | — | Адрес сервера WireGuard | | `WG_ADDRESS` | — | Адрес интерфейса WireGuard | | `WG_DNS` | — | DNS для WireGuard | +| `SHOP_ACTIVATED` | — | Флаг активации магазина. false = магазин заблокирован до активации супер-админом в админке (Настройки → Shop Activation). | Генерация ключа шифрования: ```bash @@ -194,8 +202,9 @@ Docker автоматически собирает нативные модули # Запуск docker compose up -d -# Пересборка после изменений -docker compose up -d --build +# Пересборка (dev-сборка локально) или обновление до новой версии из registry +docker compose up -d --build # dev: собрать локально +IMAGE_TAG=1.2.9 docker compose up -d # prod: взять конкретную версию из registry # Логи docker compose logs -f diff --git a/VERSION.md b/VERSION.md index a6ebce2..fed19ce 100644 --- a/VERSION.md +++ b/VERSION.md @@ -8,10 +8,17 @@ ## Current Version -**v1.2.8** — 2026-08-09 +**v1.2.9** — 2026-08-11 ## Changelog +### v1.2.9 — 2026-08-11 +- **ci**: релиз переведён на Gitea Container Registry — образы бота и админки собираются в CI (Gitea Actions, buildx multi-arch linux/amd64+arm64 на x86_64 хосте) и пушатся как `git.softuniq.eu/telegram-market/telegram-shop` и `.../telegram-shop-admin` +- **deploy**: `docker-compose.yml` больше не собирает образы на сервере — бот и админка тянутся из registry (`IMAGE_TAG` для пиннинга версии); tor-proxy остаётся локальной сборкой (минимальный alpine) +- **install**: `install.sh` не компилирует нативный код на слабом железе — только `docker compose pull` + `up -d` (Orange Pi / RPi) +- **ci**: добавлен новый buildx-runner `gitea-host-buildx` (label `buildx`) — сборка multi-arch вместо нативной ARM64 сборки на Orange Pi +- **ci**: `REGISTRY_TOKEN` обновлён на package-токен (`write:package`) — теперь push в registry проходит без ошибок авторизации + ### v1.2.8 — 2026-08-09 - **chore**: repo cleanup — removed stale `docs/admin-frontend-spec.md` (old Express/EJS admin), unused `templates/` (SmartAdmin copy), dead `scripts/sync-agents.cjs`, committed `admin-next/.zscripts/dev.pid` - **docs**: added `docs/API.md` (admin REST API reference) and `docs/DATABASE.md` (DB schema); README structure updated diff --git a/docker-compose.yml b/docker-compose.yml index bda2ee7..d64b19c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,17 @@ +# Telegram Shop — production compose +# Образы НЕ собираются на сервере — тянутся из Gitea Container Registry: +# git.softuniq.eu/telegram-market/telegram-shop (бот, порт 3001) +# git.softuniq.eu/telegram-market/telegram-shop-admin (Next.js админка, порт 3000) +# +# Сборка/релиз: .gitea/workflows/release.yaml (Gitea Actions, buildx multi-arch). +# Для обновления: укажите IMAGE_TAG (по умолчанию latest) и docker compose up -d. +# +# Пример: +# IMAGE_TAG=1.2.9 docker compose up -d + services: telegram_shop_prod: - build: - context: . - dockerfile: ./Dockerfile + image: git.softuniq.eu/telegram-market/telegram-shop:${IMAGE_TAG:-latest} hostname: telegram_shop_prod container_name: telegram_shop_prod ports: @@ -10,6 +19,7 @@ services: restart: unless-stopped environment: - ADMIN_CHAT_URL=http://tg_shop_admin:3000/api/chat + - SHOP_ACTIVATED=${SHOP_ACTIVATED:-true} volumes: - ./db:/app/db/ - ./uploads:/app/uploads/ @@ -35,9 +45,7 @@ services: # Новая Next.js админ-панель (Prisma, одна БД с ботом) tg_shop_admin: - build: - context: ./admin-next - dockerfile: ./Dockerfile + image: git.softuniq.eu/telegram-market/telegram-shop-admin:${IMAGE_TAG:-latest} container_name: tg_shop_admin ports: - "3000:3000" @@ -73,9 +81,11 @@ services: - GITEA_API_URL=${GITEA_API_URL} - WG_PRIVATE_KEY=${WG_PRIVATE_KEY} - WG_PRESHARED_KEY=${WG_PRESHARED_KEY} + - SHOP_ACTIVATED=${SHOP_ACTIVATED:-true} volumes: - ./db:/app/db - ./uploads:/app/uploads + - ./tor-proxy/hosts:/onion-hosts:ro deploy: resources: limits: @@ -93,6 +103,7 @@ services: - tor_proxy_net tor-proxy: + # Миниатюрный alpine-контейнер (tor + entrypoint) — собирается локально за секунды build: context: ./tor-proxy dockerfile: Dockerfile diff --git a/install.sh b/install.sh index 0374aaa..8279271 100755 --- a/install.sh +++ b/install.sh @@ -155,40 +155,138 @@ else fi # ============================================================ -# 4. Проверка обязательных переменных +# 4. Настройка переменных окружения (интерактивно) # ============================================================ -print_step "Проверка переменных окружения" +print_step "Настройка переменных окружения (интерактивно)" -MISSING_COUNT=0 -MISSING_LIST="" -for VAR in BOT_TOKEN ADMIN_IDS ENCRYPTION_KEY; do - VAL=$(grep -E "^${VAR}=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' | tr -d '\r' || true) - if [ -z "$VAL" ] || [ "$VAL" = "your_bot_token_here" ] || [ "$VAL" = "123456789,987654321" ]; then - MISSING_COUNT=$((MISSING_COUNT + 1)) - MISSING_LIST="$MISSING_LIST $VAR" +# Функция для обновления переменной в .env (безопасно для спецсимволов в значении) +update_env_var() { + var_name="$1" + var_value="$2" + if grep -q "^${var_name}=" .env 2>/dev/null; then + awk -v key="$var_name" -v val="$var_value" ' + BEGIN { FS = OFS = "="; done = 0 } + /^[A-Za-z_][A-Za-z0-9_]*=/ { + if ($1 == key) { print key "=" val; done = 1; next } + } + { print } + END { if (!done) print key "=" val } + ' .env > .env.tmp && mv .env.tmp .env + else + echo "${var_name}=${var_value}" >> .env fi -done +} -if [ "$MISSING_COUNT" -ne 0 ]; then - print_warn "Не заполнены или имеют placeholder значения:$MISSING_LIST" +# Скрытый ввод секретов (POSIX sh, работает в Alpine ash) +read_secret() { + printf "%s" "$1" + stty -echo 2>/dev/null + read -r input + stty echo 2>/dev/null printf "\n" - printf "${YELLOW}Откройте .env в редакторе и заполните:${NC}\n" - for V in $MISSING_LIST; do - printf " - %s\n" "$V" - done - printf "\n" - printf "Продолжить всё равно? (y/N): " - read -r CONTINUE - case "$CONTINUE" in - [Yy]|[Yy][Ee][Ss]) - ;; - *) - print_info "Откройте .env, заполните значения и запустите install.sh снова." - exit 0 - ;; - esac +} + +# Функция для генерации случайного hex-строки (32 байта) +generate_random_hex() { + if command -v node >/dev/null 2>&1; then + node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" + elif command -v openssl >/dev/null 2>&1; then + openssl rand -hex 32 + else + od -An -tx32 /dev/urandom | tr -d ' \n' + fi +} + +# BOT_TOKEN +current_bot_token=$(grep -E "^BOT_TOKEN=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true +if [ -z "$current_bot_token" ] || [ "$current_bot_token" = "your_bot_token_here" ]; then + print_warn "BOT_TOKEN не задан или содержит placeholder." + read_secret "Введите BOT_TOKEN (от @BotFather): " + update_env_var "BOT_TOKEN" "$input" + bot_token="$input" else - print_ok "Все обязательные переменные заполнены" + bot_token="$current_bot_token" +fi + +# ADMIN_IDS +current_admin_ids=$(grep -E "^ADMIN_IDS=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true +if [ -z "$current_admin_ids" ] || [ "$current_admin_ids" = "123456789,987654321" ]; then + print_warn "ADMIN_IDS не задан или содержит placeholder." + printf "Введите ADMIN_IDS (через запятую, например: 123456789,987654321): " + read -r input + update_env_var "ADMIN_IDS" "$input" + admin_ids="$input" +else + admin_ids="$current_admin_ids" +fi + +# ENCRYPTION_KEY +current_encryption_key=$(grep -E "^ENCRYPTION_KEY=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true +if [ -z "$current_encryption_key" ]; then + encryption_key=$(generate_random_hex) + update_env_var "ENCRYPTION_KEY" "$encryption_key" + print_ok "ENCRYPTION_KEY сгенерирован автоматически" +else + encryption_key="$current_encryption_key" +fi + +# ADMIN_SECRET +current_admin_secret=$(grep -E "^ADMIN_SECRET=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true +if [ -z "$current_admin_secret" ] || [ "$current_admin_secret" = "changeme" ]; then + admin_secret=$(generate_random_hex) + update_env_var "ADMIN_SECRET" "$admin_secret" + print_ok "ADMIN_SECRET сгенерирован автоматически" +else + admin_secret="$current_admin_secret" +fi + +# SUPER_ADMIN_SECRET +current_super_admin_secret=$(grep -E "^SUPER_ADMIN_SECRET=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true +if [ -z "$current_super_admin_secret" ] || [ "$current_super_admin_secret" = "changeme_super" ]; then + # Генерация до тех пор, пока не получим значение, отличное от ADMIN_SECRET + super_admin_secret="" + while [ -z "$super_admin_secret" ] || [ "$super_admin_secret" = "$admin_secret" ]; do + super_admin_secret=$(generate_random_hex) + done + update_env_var "SUPER_ADMIN_SECRET" "$super_admin_secret" + print_ok "SUPER_ADMIN_SECRET сгенерирован автоматически" +else + super_admin_secret="$current_super_admin_secret" +fi + +# SHOP_ACTIVATED +current_shop_activated=$(grep -E "^SHOP_ACTIVATED=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true +if [ -z "$current_shop_activated" ]; then + shop_activated="false" + update_env_var "SHOP_ACTIVATED" "$shop_activated" + print_info "SHOP_ACTIVATED установлен в 'false' (магазин стартует заблокированным)" +else + shop_activated="$current_shop_activated" +fi + +# ADMIN_URL +current_admin_url=$(grep -E "^ADMIN_URL=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true +if [ -z "$current_admin_url" ]; then + # Попытка определить LAN IP + lan_ip=$(hostname -I | awk '{print $1}') + if [ -z "$lan_ip" ]; then + lan_ip=$(ip route get 1 | awk '{print $7}' 2>/dev/null) + fi + if [ -n "$lan_ip" ]; then + admin_url="http://${lan_ip}:3000" + update_env_var "ADMIN_URL" "$admin_url" + print_info "ADMIN_URL установлен в $admin_url (определен автоматически)" + else + print_warn "Не удалось определить LAN IP. ADMIN_URL не установлен." + fi +else + admin_url="$current_admin_url" +fi + +# Ограничиваем доступ к .env (содержит секреты) +if [ -f .env ]; then + chmod 600 .env + print_ok ".env защищён (chmod 600)" fi # ============================================================ @@ -199,22 +297,24 @@ mkdir -p db uploads print_ok "db/, uploads/ готовы" # ============================================================ -# 6. Сборка образа +# 6. Загрузка образов из Gitea Container Registry # ============================================================ -print_step "Сборка Docker-образа (это может занять несколько минут)" +print_step "Загрузка образов из Gitea Container Registry" print_info "Архитектура: $ARCH_NAME" -print_info "Нативные модули: better-sqlite3, tiny-secp256k1 (компилируются в builder)" +print_info "Образы собираются в CI (Gitea Actions, buildx multi-arch) — на сервере ничего не компилируется" -# Docker автоматически собирает под текущую архитектуру хоста -# buildx нужен только для multi-arch push в registry, для локальной сборки — обычный build -docker build -t telegram-shop:latest . -print_ok "Образ telegram-shop:latest собран" +# Релизная версия: по умолчанию latest, можно переопределить IMAGE_TAG=v1.2.9 +IMAGE_TAG="${IMAGE_TAG:-latest}" +print_info "Используемая версия образа: $IMAGE_TAG" + +$COMPOSE_CMD pull telegram_shop_prod tg_shop_admin +print_ok "Образы telegram-shop:${IMAGE_TAG} и telegram-shop-admin:${IMAGE_TAG} загружены" # ============================================================ # 7. Запуск контейнеров # ============================================================ print_step "Запуск контейнеров" -$COMPOSE_CMD up -d +IMAGE_TAG="${IMAGE_TAG}" $COMPOSE_CMD up -d print_ok "Контейнеры запущены" # ============================================================ @@ -257,7 +357,7 @@ else fi # ============================================================ -# 9. Tor Proxy — onion-адреса +# 11. Tor Proxy — onion-адреса # ============================================================ printf "\n" print_step "Проверка Tor-прокси" @@ -283,7 +383,7 @@ if [ "$TOR_RUNNING" -eq 1 ]; then printf "\n ${BOLD}Подключение:${NC}\n" printf " SSH: torify ssh root@%s\n" "$SSH_ONION" printf " Admin: откройте http://%s в Tor Browser\n" "$ADMIN_ONION" - + if [ -f "./tor-proxy/get-onions.sh" ]; then sh ./tor-proxy/get-onions.sh 2>/dev/null || true fi @@ -295,6 +395,35 @@ else printf " docker logs tor-proxy\n" fi +# ============================================================ +# 12. Финальная сводка +# ============================================================ +printf "\n" +print_step "Финальная сводка" + +# LAN IP для админки (если не определен ранее) +if [ -z "${admin_url:-}" ]; then + lan_ip=$(hostname -I | awk '{print $1}') + if [ -z "$lan_ip" ]; then + lan_ip=$(ip route get 1 | awk '{print $7}' 2>/dev/null) + fi + if [ -n "$lan_ip" ]; then + admin_url="http://${lan_ip}:3000" + else + admin_url="Не определен" + fi +fi + +printf " ${CYAN}${BOLD}LAN URL админки:${NC} %s\n" "$admin_url" +printf " ${CYAN}${BOLD}SSH onion:${NC} %s\n" "$SSH_ONION" +printf " ${CYAN}${BOLD}Admin onion:${NC} %s\n" "$ADMIN_ONION" +printf " ${CYAN}${BOLD}ADMIN_SECRET:${NC} %s\n" "$admin_secret" +printf " ${CYAN}${BOLD}SUPER_ADMIN_SECRET:${NC} %s\n" "$super_admin_secret" +printf "\n" +printf "${YELLOW}⚠ Магазин стартует заблокированным (SHOP_ACTIVATED=false).${NC}\n" +printf "${YELLOW} Войдите как супер-админ, добавьте комиссионные кошельки и активируйте магазин в Настройках.${NC}\n" +printf "\n" + # ============================================================ # Готово # ============================================================