From 0c167c8f096a7971f649271f8a4328ba8d524d4f Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Tue, 26 May 2026 17:26:46 +0300 Subject: [PATCH] docs: refactor docs structure --- .dockerignore | 2 +- .gitignore | 2 +- README.md | 25 +-- deploy/examples/README.md | 39 +--- deploy/examples/caddy/README.md | 32 +--- deploy/examples/newt/README.md | 34 +--- deploy/examples/nginx/README.md | 43 +---- deploy/examples/nginx/ssl/README.md | 9 +- deploy/examples/no-proxy/README.md | 24 +-- docs-site/DEPLOY.md | 45 ----- docs-site/astro.config.mjs | 100 ++++++++--- docs-site/public/favicon.png | Bin 0 -> 32616 bytes docs-site/public/favicon.svg | 5 - docs-site/public/favicon.webp | Bin 0 -> 7492 bytes docs-site/scripts/sync-docs.mjs | 169 ++++++++++++++---- docs-site/src/assets/logo.svg | 7 - docs-site/src/content/docs/index.md | 32 ---- docs-site/src/styles/custom.css | 50 +++++- docs/administration/maintenance.md | 27 +++ docs/administration/users.md | 19 ++ docs/configuration.md | 12 +- docs/{ => configuration}/env-vars.md | 2 +- docs/configuration/security.md | 37 ++++ docs/deploy-examples/caddy.md | 39 ++++ docs/deploy-examples/index.md | 39 ++++ docs/deploy-examples/newt.md | 38 ++++ docs/deploy-examples/nginx.md | 44 +++++ docs/deploy-examples/no-proxy.md | 35 ++++ docs/deployment.md | 26 +-- docs/{admin.md => features/admin-panel.md} | 0 docs/features/core.md | 22 +++ docs/features/payments.md | 28 +++ docs/features/subscriptions.md | 21 +++ docs/{ => features}/support.md | 2 +- docs/{ => features}/tariffs.md | 4 +- docs/{webapp.md => features/web-app.md} | 14 +- docs/{ => features}/webapp-themes.md | 0 docs/{ => features}/webapp-themes.webp | Bin docs/getting-started/overview.md | 26 +++ docs/getting-started/setup.md | 38 ++++ docs/index.md | 32 ++++ docs/migrations/index.md | 27 +++ .../remnawave-tg-shop.md} | 10 +- docs/payments/cryptopay.md | 28 +++ docs/payments/freekassa.md | 16 ++ docs/payments/heleket.md | 31 ++++ docs/payments/platega.md | 30 ++++ docs/payments/severpay.md | 28 +++ docs/payments/telegram-stars.md | 19 ++ docs/payments/wata.md | 30 ++++ docs/payments/yookassa.md | 16 ++ docs/troubleshooting/issues.md | 33 ++++ docs/troubleshooting/logs.md | 104 +++++++++++ tests/test_migration_doc_accuracy.py | 10 +- 54 files changed, 1154 insertions(+), 351 deletions(-) delete mode 100644 docs-site/DEPLOY.md create mode 100644 docs-site/public/favicon.png delete mode 100644 docs-site/public/favicon.svg create mode 100644 docs-site/public/favicon.webp delete mode 100644 docs-site/src/assets/logo.svg delete mode 100644 docs-site/src/content/docs/index.md create mode 100644 docs/administration/maintenance.md create mode 100644 docs/administration/users.md rename docs/{ => configuration}/env-vars.md (99%) create mode 100644 docs/configuration/security.md create mode 100644 docs/deploy-examples/caddy.md create mode 100644 docs/deploy-examples/index.md create mode 100644 docs/deploy-examples/newt.md create mode 100644 docs/deploy-examples/nginx.md create mode 100644 docs/deploy-examples/no-proxy.md rename docs/{admin.md => features/admin-panel.md} (100%) create mode 100644 docs/features/core.md create mode 100644 docs/features/payments.md create mode 100644 docs/features/subscriptions.md rename docs/{ => features}/support.md (96%) rename docs/{ => features}/tariffs.md (99%) rename docs/{webapp.md => features/web-app.md} (94%) rename docs/{ => features}/webapp-themes.md (100%) rename docs/{ => features}/webapp-themes.webp (100%) create mode 100644 docs/getting-started/overview.md create mode 100644 docs/getting-started/setup.md create mode 100644 docs/index.md create mode 100644 docs/migrations/index.md rename docs/{migration-to-minishop.md => migrations/remnawave-tg-shop.md} (94%) create mode 100644 docs/payments/cryptopay.md create mode 100644 docs/payments/freekassa.md create mode 100644 docs/payments/heleket.md create mode 100644 docs/payments/platega.md create mode 100644 docs/payments/severpay.md create mode 100644 docs/payments/telegram-stars.md create mode 100644 docs/payments/wata.md create mode 100644 docs/payments/yookassa.md create mode 100644 docs/troubleshooting/issues.md create mode 100644 docs/troubleshooting/logs.md diff --git a/.dockerignore b/.dockerignore index ea5a02f..9f817d2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -16,7 +16,7 @@ frontend/node_modules/ docs-site/node_modules/ docs-site/.astro/ docs-site/dist/ -docs-site/src/content/docs/reference/ +docs-site/src/content/docs/ deploy/compose/docker-compose-dev.yml data/* !data/tariffs.example.json diff --git a/.gitignore b/.gitignore index 33ba041..3150f45 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ node_modules/ # Documentation site build artifacts docs-site/.astro/ docs-site/dist/ -docs-site/src/content/docs/reference/ +docs-site/src/content/docs/ # WebApp build artifacts (regenerated by `npm run build:webapp` / Docker build) bot/app/web/templates/subscription_webapp.css diff --git a/README.md b/README.md index 9fa59b8..1dfb662 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Remnawave Minishop - Telegram-бот и Web App (Mini App) для продажи и управления подписками панели [Remnawave](https://docs.rw/). Бот обрабатывает регистрацию, оплату, продление, пробный период, промокоды, рефералов и поддержку в чате. Web App показывает ссылку подключения, срок действия, трафик, оплату, устройства и вход по Telegram Mini Apps `initData`, Telegram OAuth / OpenID Connect и одноразовому email-коду. -Проект является переработанным форком [kavore/remnawave-tg-shop](https://github.com/kavore/remnawave-tg-shop). Для переноса данных из прежнего стека используйте [инструкцию по миграции](docs/migration-to-minishop.md). +Проект является переработанным форком [kavore/remnawave-tg-shop](https://github.com/kavore/remnawave-tg-shop). Для переноса данных из прежнего стека и других ботов используйте [раздел миграций](docs/migrations/index.md). ## Возможности @@ -32,15 +32,18 @@ Remnawave Minishop - Telegram-бот и Web App (Mini App) для продажи ## Документация +- [Входная страница документации](docs/index.md) - маршрут по установке, настройке, платежам, админке и диагностике. +- [Deploy examples](docs/deploy-examples/index.md) - готовые варианты запуска: Caddy, Nginx, Pangolin/Newt и no-proxy. - [Настройка окружения](docs/configuration.md) - bootstrap `.env` и рекомендуемая настройка через Web App админку. -- [Переменные `.env`](docs/env-vars.md) - полный справочник всех env-ключей по разделам. -- [Тарифы](docs/tariffs.md) - каталог тарифов, period- и traffic-модели, обычные и premium-докупки, premium-сквады, смена тарифа, HWID-лимиты и обработка трафика. -- [Админ-панель](docs/admin.md) - права доступа, настройки, редактор тарифов, premium-сквады и сохранение JSON-каталога. -- [Web App / Mini App](docs/webapp.md) - отдельный порт, домен, Telegram OAuth, email-вход, инструкции установки и реферальные ссылки. -- [Поддержка](docs/support.md) - тикеты в Mini App, входящий список админки, уведомления, лимиты и внешняя ссылка поддержки. -- [Темы Web App](docs/webapp-themes.md) - кастомные темы, настройка внешнего вида, логотипы, CSS/ассеты и пайплайн создания новой темы. +- [Переменные `.env`](docs/configuration/env-vars.md) - полный справочник всех env-ключей по разделам. +- [Тарифы](docs/features/tariffs.md) - каталог тарифов, period- и traffic-модели, обычные и premium-докупки, premium-сквады, смена тарифа, HWID-лимиты и обработка трафика. +- [Админ-панель](docs/features/admin-panel.md) - права доступа, настройки, редактор тарифов, premium-сквады и сохранение JSON-каталога. +- [Web App / Mini App](docs/features/web-app.md) - отдельный порт, домен, Telegram OAuth, email-вход, инструкции установки и реферальные ссылки. +- [Поддержка](docs/features/support.md) - тикеты в Mini App, входящий список админки, уведомления, лимиты и внешняя ссылка поддержки. +- [Темы Web App](docs/features/webapp-themes.md) - кастомные темы, настройка внешнего вида, логотипы, CSS/ассеты и пайплайн создания новой темы. - [Развертывание](docs/deployment.md) - Docker Compose, reverse proxy, Nginx, Caddy, вебхуки, запуск из образа и обновление версии (`IMAGE_TAG`). -- [Миграция с remnawave-tg-shop](docs/migration-to-minishop.md) - перенос данных из прежнего стека. +- [Миграции](docs/migrations/index.md) - готовые сценарии переноса с других ботов; сейчас описан `remnawave-tg-shop`. +- [Миграция с remnawave-tg-shop](docs/migrations/remnawave-tg-shop.md) - готовый сценарий для legacy-стека. ## Совместимость @@ -88,9 +91,9 @@ docker compose logs -f backend worker frontend - `PANEL_API_URL`, `PANEL_API_KEY`, `PANEL_WEBHOOK_SECRET` - доступ к Remnawave; - остальные настройки удобнее задать в Web App админке. -После первого входа в админку настройте тарифы, платежные провайдеры, внешний вид, поддержку, уведомления и инструкции подключения через UI. Инструкции установки включены по умолчанию, читают Subscription Page config из Remnawave Panel и при проблемах с конфигом откатываются к обычной ссылке подключения. Полный справочник env-переменных: [docs/env-vars.md](docs/env-vars.md). +После первого входа в админку настройте тарифы, платежные провайдеры, внешний вид, поддержку, уведомления и инструкции подключения через UI. Инструкции установки включены по умолчанию, читают Subscription Page config из Remnawave Panel и при проблемах с конфигом откатываются к обычной ссылке подключения. Полный справочник env-переменных: [docs/configuration/env-vars.md](docs/configuration/env-vars.md). -Для каталога тарифов используется `TARIFFS_CONFIG_PATH` со значением по умолчанию `data/tariffs.json`. Пример формата лежит в [data/tariffs.example.json](data/tariffs.example.json), подробности - в [docs/tariffs.md](docs/tariffs.md). +Для каталога тарифов используется `TARIFFS_CONFIG_PATH` со значением по умолчанию `data/tariffs.json`. Пример формата лежит в [data/tariffs.example.json](data/tariffs.example.json), подробности - в [docs/features/tariffs.md](docs/features/tariffs.md). Если в Docker Compose включаете bind mount `./data:/app/data`, заранее создайте каталог и отдайте его пользователю контейнера. Это нужно для сохранения `data/tariffs.json`, каталога тем `data/themes`, кеша логотипа Web App и animated emoji: @@ -120,7 +123,7 @@ docker compose up -d IMAGE_TAG=3.1.0 docker compose up -d ``` -Для production-запуска удобнее брать готовые папки из [`deploy/examples`](deploy/examples): там отдельно собраны варианты для Caddy, Nginx, Newt/Pangolin и прямой публикации портов без reverse proxy. В каждой папке рядом лежат `docker-compose.yml`, `.env.example`, README и нужный proxy-конфиг. +Для production-запуска удобнее брать готовые папки из [`deploy/examples`](deploy/examples), а читать каноничные инструкции в [docs/deploy-examples/index.md](docs/deploy-examples/index.md): там отдельно описаны варианты для Caddy, Nginx, Newt/Pangolin и прямой публикации портов без reverse proxy. В папках рядом с compose лежат только конфиги и короткие ссылки на документацию. GHCR image names for releases: diff --git a/deploy/examples/README.md b/deploy/examples/README.md index f79ea0a..67870a6 100644 --- a/deploy/examples/README.md +++ b/deploy/examples/README.md @@ -1,33 +1,12 @@ -# Готовые варианты запуска +# Deploy examples -В этой папке лежат самодостаточные compose-примеры. Каждый вариант запускается из своей директории обычной командой: +Каноничная документация по вариантам запуска живет в [docs/deploy-examples/index.md](../../docs/deploy-examples/index.md). -```bash -cp .env.example .env -nano .env -docker compose up -d -``` - -После старта полезно проверить: - -```bash -docker compose ps -docker compose logs -f backend worker frontend -``` - -## Какой вариант выбрать - -| Папка | Когда использовать | Что править | -| --- | --- | --- | -| [`caddy`](caddy) | Нужен самый простой публичный HTTPS с автоматическими сертификатами Let's Encrypt. | `.env`; при нестандартной схеме можно поправить `Caddyfile`. | -| [`nginx`](nginx) | Уже используете Nginx и готовы положить TLS-сертификаты рядом с примером. | `.env`, `nginx.conf.template`, файлы в `ssl/`. | -| [`newt`](newt) | Публикуете сервисы через Pangolin/Newt без входящих портов на сервере приложения. | `.env` и ресурсы в панели Pangolin. | -| [`no-proxy`](no-proxy) | Нужно напрямую открыть порты backend/frontend или проверить стек без reverse proxy. | `.env`. | - -Для всех вариантов нужны два публичных URL: - -- webhook/backend URL для Telegram, платежных систем и Remnawave webhooks; -- Mini App/frontend URL для Telegram Mini App и Web App. - -Обычно это два домена, например `webhooks.example.com` и `app.example.com`. +Эта папка хранит только рабочие compose-примеры и конфиги. Подробное описание не дублируется здесь, чтобы сайт документации и навигация из README использовали один источник. +| Папка | Документация | +| --- | --- | +| `caddy` | [Caddy](../../docs/deploy-examples/caddy.md) | +| `nginx` | [Nginx](../../docs/deploy-examples/nginx.md) | +| `newt` | [Pangolin / Newt](../../docs/deploy-examples/newt.md) | +| `no-proxy` | [No proxy](../../docs/deploy-examples/no-proxy.md) | diff --git a/deploy/examples/caddy/README.md b/deploy/examples/caddy/README.md index e0db906..20686f2 100644 --- a/deploy/examples/caddy/README.md +++ b/deploy/examples/caddy/README.md @@ -1,31 +1,5 @@ -# Запуск с Caddy +# Caddy -Caddy сам выпускает и продлевает HTTPS-сертификаты. На сервере должны быть открыты входящие `80/tcp` и `443/tcp`, а DNS-записи `WEBHOOK_HOST` и `MINIAPP_HOST` должны смотреть на этот сервер. - -```bash -cp .env.example .env -nano .env -docker compose up -d -``` - -Минимально поменяйте в `.env`: - -- `WEBHOOK_HOST` и `MINIAPP_HOST`; -- `BOT_TOKEN`, `ADMIN_IDS`; -- `POSTGRES_PASSWORD`; -- `WEBAPP_SESSION_SECRET`, `WEBHOOK_SECRET_TOKEN`; -- `PANEL_API_URL`, `PANEL_API_KEY`, `PANEL_WEBHOOK_SECRET`. - -`Caddyfile` лежит рядом и использует домены из `.env`. Если нужна нестандартная логика Caddy, правьте его и перезапускайте: - -```bash -docker compose up -d --force-recreate caddy -``` - -Проверка: - -```bash -docker compose ps -docker compose logs -f caddy backend worker frontend -``` +Каноничная инструкция: [docs/deploy-examples/caddy.md](../../../docs/deploy-examples/caddy.md). +Файлы этого примера остаются рядом: `docker-compose.yml`, `.env.example` и `Caddyfile`. diff --git a/deploy/examples/newt/README.md b/deploy/examples/newt/README.md index 65a8c18..ef3cd4e 100644 --- a/deploy/examples/newt/README.md +++ b/deploy/examples/newt/README.md @@ -1,33 +1,5 @@ -# Запуск через Newt / Pangolin +# Pangolin / Newt -Этот вариант не открывает входящие порты на сервере приложения. Newt подключается к Pangolin, а публичные домены настраиваются ресурсами в панели Pangolin. +Каноничная инструкция: [docs/deploy-examples/newt.md](../../../docs/deploy-examples/newt.md). -```bash -cp .env.example .env -nano .env -docker compose up -d -``` - -В `.env` заполните: - -- `WEBHOOK_HOST` и `MINIAPP_HOST` - публичные домены ресурсов в Pangolin; -- `PANGOLIN_ENDPOINT`, `NEWT_ID`, `NEWT_SECRET` - значения из настроек site/client в Pangolin; -- обычные переменные приложения: `BOT_TOKEN`, `ADMIN_IDS`, `POSTGRES_PASSWORD`, секреты и доступ к Remnawave. - -Официальная инструкция Pangolin по установке Newt site: . - -В Pangolin создайте два HTTP-ресурса для этого Newt site: - -| Публичный домен | Upstream | -| --- | --- | -| `https://webhooks.example.com` | `http://backend:8080` | -| `https://app.example.com` | `http://frontend:80` | - -Домены в Pangolin должны совпадать с `WEBHOOK_HOST` и `MINIAPP_HOST`. - -Проверка: - -```bash -docker compose ps -docker compose logs -f newt backend worker frontend -``` +Файлы этого примера остаются рядом: `docker-compose.yml` и `.env.example`. diff --git a/deploy/examples/nginx/README.md b/deploy/examples/nginx/README.md index c1b1c22..675b9de 100644 --- a/deploy/examples/nginx/README.md +++ b/deploy/examples/nginx/README.md @@ -1,42 +1,5 @@ -# Запуск с Nginx +# Nginx -Этот пример поднимает Nginx в той же Docker-сети, что и приложение: - -- `WEBHOOK_HOST` проксируется в `backend:8080`; -- `MINIAPP_HOST` проксируется в `frontend:80`; -- `frontend` сам проксирует внутренние `/api`, `/auth` и ассеты тем в `backend:8081`. - -## Подготовка - -```bash -cp .env.example .env -nano .env -``` - -Положите TLS-сертификаты в `ssl/`: - -```text -ssl/ - webhooks.example.com/ - fullchain.pem - privkey.pem - app.example.com/ - fullchain.pem - privkey.pem -``` - -Имена папок должны совпадать с `WEBHOOK_HOST` и `MINIAPP_HOST` в `.env`. - -## Запуск - -```bash -docker compose up -d -docker compose logs -f nginx backend worker frontend -``` - -Если нужно поменять заголовки, лимиты или TLS-настройки, правьте `nginx.conf.template` рядом с compose и перезапускайте Nginx: - -```bash -docker compose up -d --force-recreate nginx -``` +Каноничная инструкция: [docs/deploy-examples/nginx.md](../../../docs/deploy-examples/nginx.md). +Файлы этого примера остаются рядом: `docker-compose.yml`, `.env.example`, `nginx.conf.template` и папка `ssl/` для сертификатов. diff --git a/deploy/examples/nginx/ssl/README.md b/deploy/examples/nginx/ssl/README.md index 55c952f..f1c4eeb 100644 --- a/deploy/examples/nginx/ssl/README.md +++ b/deploy/examples/nginx/ssl/README.md @@ -1,8 +1,8 @@ -# TLS-сертификаты для Nginx +# TLS certificates -Положите сюда сертификаты для доменов из `.env`. +Каноничная инструкция по Nginx: [docs/deploy-examples/nginx.md](../../../../docs/deploy-examples/nginx.md). -Пример структуры: +Кладите сертификаты в подпапки, совпадающие с `WEBHOOK_HOST` и `MINIAPP_HOST`: ```text ssl/ @@ -13,6 +13,3 @@ ssl/ fullchain.pem privkey.pem ``` - -Если используете wildcard-сертификат, можно положить одинаковые `fullchain.pem` и `privkey.pem` в обе папки. - diff --git a/deploy/examples/no-proxy/README.md b/deploy/examples/no-proxy/README.md index bd45498..2e2a6b1 100644 --- a/deploy/examples/no-proxy/README.md +++ b/deploy/examples/no-proxy/README.md @@ -1,23 +1,5 @@ -# Запуск без reverse proxy +# No proxy -Этот вариант напрямую публикует два HTTP-порта: - -- backend/webhooks: `WEB_SERVER_BIND`, по умолчанию `0.0.0.0:8080`; -- frontend/Mini App: `FRONTEND_BIND`, по умолчанию `0.0.0.0:8082`. - -```bash -cp .env.example .env -nano .env -docker compose up -d -``` - -Важно: контейнеры приложения сами не выпускают TLS-сертификаты. Для реального Telegram webhook и Mini App публичные URL должны быть HTTPS. Используйте этот вариант для локальной проверки, внутренней сети или когда HTTPS уже завершается внешней платформой и дальше трафик приходит на эти порты. - -Проверка локально: - -```bash -curl http://127.0.0.1:8080/healthz -curl http://127.0.0.1:8082/health -docker compose logs -f backend worker frontend -``` +Каноничная инструкция: [docs/deploy-examples/no-proxy.md](../../../docs/deploy-examples/no-proxy.md). +Файлы этого примера остаются рядом: `docker-compose.yml` и `.env.example`. diff --git a/docs-site/DEPLOY.md b/docs-site/DEPLOY.md deleted file mode 100644 index 74a9f0f..0000000 --- a/docs-site/DEPLOY.md +++ /dev/null @@ -1,45 +0,0 @@ -# Cloudflare Pages deploy - -This docs site is built with Astro Starlight and publishes to: - -```text -https://minishop.minidoc.cc -``` - -## Cloudflare Pages settings - -Create a Pages project connected to the GitLab repository and use: - -| Setting | Value | -| --- | --- | -| Production branch | `main` | -| Framework preset | `Astro` | -| Root directory | `docs-site` | -| Build command | `npm ci && npm run build` | -| Build output directory | `dist` | -| Node version | `22` | - -The build script runs `scripts/sync-docs.mjs` before Astro builds the site. Keep editing the canonical Markdown files in the repository-level `docs/` directory. - -## Custom domain - -After the first successful Pages deploy: - -1. Open the Pages project in Cloudflare. -2. Go to **Custom domains**. -3. Add `minishop.minidoc.cc`. -4. If `minidoc.cc` is already on Cloudflare DNS, accept the suggested DNS record and wait for TLS activation. - -## Optional API automation - -Do not use a root token for automation. Create a scoped Cloudflare API token and expose it locally as an environment variable only for the setup command. - -Minimum useful permissions: - -| Scope | Permission | -| --- | --- | -| Account | Cloudflare Pages: Edit | -| Zone: `minidoc.cc` | Zone: Read | -| Zone: `minidoc.cc` | DNS: Edit | - -Cloudflare's GitLab integration still requires the Cloudflare GitLab app/OAuth connection to be authorized for the repository. If that is not connected yet, complete the GitLab connection in the Cloudflare dashboard first, or use a direct-upload Pages workflow instead of Git-connected deployments. diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs index cccb453..0dd3ddc 100644 --- a/docs-site/astro.config.mjs +++ b/docs-site/astro.config.mjs @@ -6,23 +6,20 @@ export default defineConfig({ site: 'https://minishop.minidoc.cc', integrations: [ starlight({ - title: 'Remnawave Minishop', + title: 'minishop', + favicon: '/favicon.png', description: 'Документация по настройке, развертыванию и эксплуатации Remnawave Minishop.', plugins: [ starlightThemeNova({ nav: [ - { label: 'Запуск', href: '/reference/deployment/' }, - { label: 'Настройка', href: '/reference/configuration/' }, + { label: 'Главная', href: '/' }, + { label: 'Установка', href: '/getting-started/setup/' }, + { label: 'Платежи', href: '/features/payments/' }, { label: 'GitLab', href: 'https://gitlab.com/3252a8/remnawave-minshop' }, ], }), ], - favicon: '/favicon.svg', - logo: { - src: './src/assets/logo.svg', - alt: 'Remnawave Minishop', - }, customCss: ['./src/styles/custom.css'], lastUpdated: false, locales: { @@ -32,11 +29,19 @@ export default defineConfig({ }, }, head: [ + { + tag: 'link', + attrs: { + rel: 'icon', + href: '/favicon.webp', + type: 'image/webp', + }, + }, { tag: 'meta', attrs: { name: 'theme-color', - content: '#0f766e', + content: '#00fe7a', }, }, { @@ -49,32 +54,83 @@ export default defineConfig({ ], sidebar: [ { - label: 'Обзор', - link: '/', + label: 'Начало', + items: [ + { label: 'Главная', link: '/' }, + { label: 'Обзор', slug: 'getting-started/overview' }, + { label: 'Установка', slug: 'getting-started/setup' }, + { label: 'Deploy examples', slug: 'deploy-examples' }, + ], }, { - label: 'Запуск', + label: 'Варианты деплоя', items: [ + { label: 'Caddy', slug: 'deploy-examples/caddy' }, + { label: 'Nginx', slug: 'deploy-examples/nginx' }, + { label: 'Pangolin / Newt', slug: 'deploy-examples/newt' }, + { label: 'Без reverse proxy', slug: 'deploy-examples/no-proxy' }, + ], + }, + { + label: 'Конфигурация', + items: [ + { label: 'Переменные', slug: 'configuration/env-vars' }, { label: 'Настройка окружения', slug: 'reference/configuration' }, - { label: 'Переменные .env', slug: 'reference/env-vars' }, - { label: 'Развертывание', slug: 'reference/deployment' }, - { label: 'Миграция', slug: 'reference/migration-to-minishop' }, + { label: 'Безопасность', slug: 'configuration/security' }, ], }, { - label: 'Web App', + label: 'Возможности', items: [ - { label: 'Mini App', slug: 'reference/webapp' }, - { label: 'Темы и внешний вид', slug: 'reference/webapp-themes' }, - { label: 'Админ-панель', slug: 'reference/admin' }, - { label: 'Поддержка', slug: 'reference/support' }, + { label: 'Основные', slug: 'features/core' }, + { label: 'Платежи', slug: 'features/payments' }, + { label: 'Подписки', slug: 'features/subscriptions' }, + { label: 'Тарифы', slug: 'features/tariffs' }, + { label: 'Mini App', slug: 'features/web-app' }, + { label: 'Темы Web App', slug: 'features/webapp-themes' }, + { label: 'Админ-панель', slug: 'features/admin-panel' }, + { label: 'Поддержка', slug: 'features/support' }, ], }, { - label: 'Продукт', + label: 'Платежные системы', + items: [ + { label: 'YooKassa', slug: 'payments/yookassa' }, + { label: 'FreeKassa', slug: 'payments/freekassa' }, + { label: 'Platega', slug: 'payments/platega' }, + { label: 'SeverPay', slug: 'payments/severpay' }, + { label: 'Wata', slug: 'payments/wata' }, + { label: 'CryptoPay', slug: 'payments/cryptopay' }, + { label: 'Heleket', slug: 'payments/heleket' }, + { label: 'Telegram Stars', slug: 'payments/telegram-stars' }, + ], + }, + { + label: 'Администрирование', + items: [ + { label: 'Пользователи', slug: 'administration/users' }, + { label: 'Обслуживание', slug: 'administration/maintenance' }, + ], + }, + { + label: 'Миграции', + items: [ + { label: 'Обзор миграций', slug: 'migrations' }, + { label: 'remnawave-tg-shop', slug: 'migrations/remnawave-tg-shop' }, + ], + }, + { + label: 'Устранение неполадок', + items: [ + { label: 'Проблемы', slug: 'troubleshooting/issues' }, + { label: 'Логи', slug: 'troubleshooting/logs' }, + ], + }, + { + label: 'Справочник', items: [ - { label: 'Тарифы', slug: 'reference/tariffs' }, { label: 'Архитектура', slug: 'reference/architecture' }, + { label: 'Развертывание', slug: 'reference/deployment' }, ], }, ], diff --git a/docs-site/public/favicon.png b/docs-site/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..387b954438c2f781668e6a505089d3479c4c3939 GIT binary patch literal 32616 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 literal 0 HcmV?d00001 diff --git a/docs-site/public/favicon.svg b/docs-site/public/favicon.svg deleted file mode 100644 index 15401b8..0000000 --- a/docs-site/public/favicon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/docs-site/public/favicon.webp b/docs-site/public/favicon.webp new file mode 100644 index 0000000000000000000000000000000000000000..11b0cb39b6b67b3b6042ea49b2f74697cac8e052 GIT binary patch literal 7492 zcma)=Wl$VV!>$L{;4X{1ySpYxa1HLx;%-?i1PK~Qa0~7bfQBj-{0Q41PHS{!u4bTAqz=N$&xW7e4{iPc6-y_%p zz>d@uCr|(YRtN#$Cmc`-2b=)_f{dz$G6Yr&4x1Z_i`%Q~va0EsX%mKj$eyOh@*!z* zBGZ-mRJS{BABwo?7t`36d8$maO!8dOfQJdMUUV1VWFexHMN1N>&q(cK;yDXEdiInNB4Y$@WNcE&ovUl1p4AQHzRns4Jn7FGNA z?5}13>=~$8VFRo+1hU~+x8bVbBtIzUt1uK5=9f(GGuPsw*|}~xxrXWJ;ZTK9<$)rf zZ0~^w<5DWVP-uU?OK9m+VIW@xRPL#3rS{g#!f$X;zf6i7`^E&JtMDFp7CW%mw|WHK zEuM2}V2dUibY0wuhmbwP-HKiLZfwqA_je97={-zgE@ix%Jtm2Xpo(kws7ozhEgH+=ve4lLIDG(gj>mPl$Ec(^>&W5jMq2Fu8}c% z@((p&sie$jy`oN~Owv!rddtyNtY>drC_iDFNn|mCE6qEa6kcCcc{GeWaf!zAT^6QWdS5Aq<*bW}C@~iGi-!qYCe8FtKkHA!9T6+~Q7>a`ioZuqKz|V~ zWnl3c+NlKVw>)S^mR;r}n&PrZ*uI5GuV2@+8Gx|KikT{B+;0=!QbMx!q3Ts4F2;Z-YCYeXR@2Vp%CETSO5MAi@fn!Czy&6Uj_n@%lyjl8HDn zgKvP$fbDOfPD7~y+p@6kK6oh8VD~TIwjnqe3;*3)I2-2try1qKaeCvQM~_N^bLF2f zzJ~$NPZh{iE2#o>4ZBY2b)?c$6LO!(d9EU-z;`pqdr{^4U9IeKAc+huFWfmf#f&^{ z!MjJhnQP=GAg%2yyP0R@gy5made2ReRLXYIi&+o9Si*-z@I{#Zmwqu4faTBhxp3^3 zK_N?sG;cEeW%)-0sHkwyk=t_gl@6NDmNyP4)im5`fWwA&P<@NkdgF{z%C zZE1lI1DJT}lPqITtagLKfc_y+*wP~kTNgU+nss^Q;4g2_m;#NYmJ1~cj)gS^d4lEe z$Ui7ohGEXm8IHWSJ>!<#ugRj8N71_QPF1}5k^3v6cI+3+POt8B&@1QE!%^pyqx1_Q9>9Gq@lZ!Xew0Lip3PfN*HhSzs7N zcN)oU+=@e;7ks$Yf@JuRpT7;drBnZQ$Te2-ba?QXploVPcx$b3&);AGFg)ca=Btt~ zqz-U5T8mZlX2l{+yD@h?id;^BBl36DK^?9 zxPzAu_e7KF=Bzt(cM&M1{+H^PFv!E83ETdc>TrLl?)hJ;N9bA`yKtH)7ST_{=+K{K znjWC~;XdU8MGs;_8s}f%ej&vT=3x#0VqPI%^WY|Tf`!hn^5)Dp*rVSYI*1t#r69u* zpZP(_Z$qM)uP@pkljjD=C^T}6L*w=|jFzhKY9yw%whKm|#Z2kNZhKT3j^CqWS z45dl06Qf(eF^hw1QsuooP5lhl=f8?0tlztUUj_?9&FBB=6i-qOPl|PQw*-y5KNT+5 z{$vneeZud$?BwJHq&|%A7%Qz#%9bHz$=c+#zVTKo^3x;n`NzZy8hbs81(Z(zs2}A9^Y4MM_n=!7#trYocf)G%u9mlOdFXJ?%E`c z;F=G+*4X+t=_@(?T_)Lk*gf=8xo;jU_fm$=S98n>OPy8HSx)#D9$UEWUhXQ-X&1Or z-ERo~@VU#pry{YhL@b|s)#T}#><^I_$uYKyeC0sP{~%Ya9*{*%+T@ib6&yW5B+Da| z<7llG)L=QJaIbEr|4@d{`n{;DYgP$j@^2hF%BXmp8&E96?Uj=KqywGnvSaNP;-)Z* zTk$~cocPwXd5n?!20GcqRbEJ};rKki@5< zNq(2RLbq}95T{I)^zz~_ukr^(9Efp;#-AS(*$hS-N5bafCU-+i+aU{CwT7k`#_nRR zVK`m)C~}`>C1`W!H?@|qBo@rsnp9WySHyb#2^D(qv>%ms!CI->ei8KqFY(lJikupp zG|9I}*!Abre;SpE4*L`!3uS4DZ;f|o3Hux3ZLyPj)HC#o7^U4)hfr1sc`XsUXA~!8 z@(k;xfpfykGeh}~1u{DI0DAftaGtHd7ipdVBpqZOC+-7t+Hdv_>?}A}ovEuvc~PZz zQH&OE-%$c{ORNpG*}vNtNXlESg4^)WCH%S}v>esAE&dYG_v9D>jn?gDGe_b}@Oes# z@-@~p@4{HlQ7JOJ!Ov&9*u3{BYdUVq?KG6Q8J;EA?QWqSLr90eoiH5b-}_Hbg00Jv z^+KJaWH1t980D7bcjJN@!%sZ@V~`X`8tFA<`28-^k1X9NZg38*rDWa=Q7iyzrWBCu zBTmE#XVL5wJFR0YCg}ayWls=N5#|p7$lf=IWx7yqobQhu1shF*}WG3a*x}KesMDL#?KyxC_TNbZ=qu6-;8m3Rd~L=19LS;Ort3ytILRN zkbRPvpC=tx^`E#YvZjORHz7b)@(_C90qN*Gg#>vXvfQtBhD z-`p2jXNo1&Vq(+h1KTU6R9HTdBze~aBYkb0%n`l1rhm6hKQlH z1qIN?wNm+F7%U{}y(du+cU&mPJ&N<_WA)6uaTpsLivi#*LKK~jy1Jk@j$F}Ng5s|F z=w>hX7pCnrEwQ(|iSr}q0c*~)$`p-AToatGkYIAN3Ez@7}0$&JIZ3J zBtD@NwJsp4W;)Ux3S%gCghwCyWwl^?d(pHQRezZT-Han`H2A#oZi&JTgC6el63T&c zA`jeY_|oA;oXt({Nk&Sv-sTC_WnW7Rrs$PpAH6pTWOI3nShCW?%S! z*#|RPW3Z=TrnUU5Voiq+5r~pvwYw@r?f(++$4xBR`^TaMOItfqJmq8o;h?9S;{=vGTA3t>0rh+vY|=ozTjyhn!^CRV?js7RR-L=Lp(+izi8Gz z8jZdwz;HEl?$LnroJW+w`iF$gFcRwM{BII^p8bb}lXL$eVZtH!Vi5YMSGqB_s?(YK z;08?8kFi|9eLg*sED7qC`WfJV$+e3xkut-jP+HbgaU19HQ;{hG)qdFN4llGcSk*rXkghWp5E51Ze*~{gk2>Q!d~>)iaW>mCA<4_XHQzV8 zg{Q3ff}?9rV!#Gw>woXfg>1FZ(yD`Vc%R=`Z7&r(6R(fs?GKX)`E;fED$F|!kzgmS zoroaFy2cuL_2BaqwtulD#${U!AHQ%OT|x5eilDLkB45>sVlo@QhQ|6PvV@n<)Fhrb z-Lz)cJY?l-tm(I-(vyVHH-tZy5txn|(d>T7I5snEDXVsFz=6h*)V=J8EX4Aa4;c2) zMWliAr_hMj7s=ODS+ZZG%j!4vK;BEbBW7Ff86^JLv2WhtB}0oc?P)pp{N&;83Vp!? z!%XHcTH$xp{^mpwKHvKHHLLs9r9izpVFL~nmH6(?XI0=d0FO#c>s;1FpB!I?ZXLF8 z>ICY@*qRgO*gdPE*GE}KpsSU59t%gSNna2y3Y^>%=Nn2;Cn7$RZpAmC<;KY>`8PXl zo9>|Sl-0&O^aXKP{>o=8l=R=kXLB$rRln7i=-vY`EM`yyL1;S!NU8^_DHDyh3y=l; z)n{hfD!n%`xfEvzUWdxWUS!#4##w?9D7dgToV)WWK;m7<8xFw}<`>~Cw+Itea6=1@ zA%XLg<>U$@Ux&?2nuAt80zFkRtomMwG3@Z{h^^h~e)VBGhMn`%#y%>`++?EsDexq% zn`-&VSrSI}cIFd|a?VJ0=2nG95|oh2#nFuvgdPXypYiQR-)>0wQjCal(XxW5vM@@U zx?VjD3sa~?h9=qC2`yUt7CDA zQdW2v%kypmYVinGy$AWFyQ%Evfn@+%x!3!7Mr}QcyCMX|8fn1~$Z3lRdSk(bLU#`< zMxQZIg7pk%FcBniwF_U-nh1%S1Hii?3|RoKT~?ae<2_R(CGgY0;Jz!wZ6VAl*(X_G z?^ApCFX!|R*sA4^T;gB6Hsjf`{KI#@HP(Vsou3L;jJ9p`{lZKk=62T*oDdd zpmLVPq|2O-z6|T|(N}IkS0;y8MB)J>ap#SQaNlb`&QdlAxA~he9`*&N?2gfJoMGK+ zRDayBh&@7RidxhOVG`RRe8u<}GXVBhDb=%#m}>FcDw-YfLX(`DHat`-c_*O3zZT*O{jx(&B(LaYsCOP`QWiyuZkp`vO zi{NvCN!O>qQ{~Ngdnz{ejF2Gej_-lz0K%ASM#WaA<1>pweKe=$^b^O}p7H4xJhLpO zQs2U-Cj>pGc!h!j(=>e4Syjrs$5xv#iZX4E&EV^jujY-NR@xXhH(c#%KGj$7 zM`xkA;p;AoX#8i(kxmk3t%8Nh4l{ctk8f?|yfzoFK0SSX{u=Uu{QGR4QAVAtB9qE!R493sDe~j1vbxWN8!B^m~TY_QN|NUbObIVmV zS4INTO~lVg4$83*)I869XnV}_>#&JZ((Dro+qskyWT>>?j(gTk_|NyIH3FPPcmDWM zi=Bn9gxd!*@k{PTpN)%)^KnavQ481diPY&;bsRUeUYoXP|Cr^A&}Vl)@Ol@IifnD8rENB zGQ0hr^r+!JlXJp^^y_c%LVnAhm5b|-x9?ygnef*yU2ONbdK$YW{=K}duUq`5Hq5Vi z*v!Wz(b%#*NzsE;-a9^UR5uaDyZX0DsMQ+IxT=oCB2_p`4eMdkyq#!2;yVv}uc?Cq zd0pMVU93;D65Z>urx_=Fr>6b=*PhgUHxKp50?^LhVLRD*aN8bPDE?Kd(nd~;x|11% zer0T(d0c}z^gvkU))Odm%V$;o@PG-6>XOatvbGFll(z^yhGIiM$o)x|3+H`8_d({b z1!w6*K71aUu>O+nn&uQBSYalpq3SC?l4WB#qqo8lFg>k3>@pqO-uo-+qMjz) zJ_zOZO-VUgf@7vOxx$0w-rLq^%Nt{@&m7btb}2&KzD6nTIYsA_?{gwxzQ<$t1MwCi z_d|A=NlEPrCgO_4r<10XjLiiui!bWOqJ4$$wa|TqW;S!$4&!`jbon4$RT=D#M7${j zu-U0Godm5J*@K#9sF`(5J1Phh8`QRq4gTUVBy2)aDn_L;Kkrn&s9T+!&951fpq-rv zUiho_3C-7W{wMkE?Vo#K0{QZqp{Kx86bA z(BJBTQ3Jh_$!wBF*nh5M65rJ~qW>dFE;X`D?;r4Z2~9-NeLI>LTs%Az{BH%#j?>-u zv}x3NGlo3dnxQ>?GK7~H`jL9?9U#9~IZ88j;I3q1mFsF^7MWhQH4*I3d72hvlf^$- z5_Iic5y)md#-s98$54?W&LyaWRy7L^JP{IJM8}W#88b6O^Vq8LseuT|XYqkZw9e-R zcX&)?7A+|wR0N?EBN0ly_Dc;?TMbowx1L9s>9ncz<+2!?<$eLo^*5sTDNI^`V;8(B{{#}-EJkD$L z`&H>;7H(X&*SAEQugwK&(KwL?S&UdCCn9{8Nss%fJQ`mlY5bc1rJO-OY|y=Rz}Jh? zXAcmaI33}0P&fg>=FDIXO~q=}V6M`oSlM_)w z(^|Yu4BWA#Ww-T5%hC7GJ^*yUJV*={OZxFnAPmj6VKQ>0oqRI6bUVY~$cstI;Efv_do)c=< zaz5eVtb?I2BP8er;C(Bhq4Oiq#OwP}@m;!(xwCwHGGkclqkIG|7f2RiU;>h8-8n}A zU}BJt)^P*=dY38WNx=JwA_H!~Ps8-oJ_$)dH?N!(iso$EkiccoRPf_I3;_NQt_L{| literal 0 HcmV?d00001 diff --git a/docs-site/scripts/sync-docs.mjs b/docs-site/scripts/sync-docs.mjs index f3d663d..b91ae97 100644 --- a/docs-site/scripts/sync-docs.mjs +++ b/docs-site/scripts/sync-docs.mjs @@ -5,19 +5,44 @@ import { fileURLToPath } from 'node:url'; const siteRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url))); const repoRoot = path.resolve(siteRoot, '..'); const sourceDir = path.join(repoRoot, 'docs'); -const outputDir = path.join(siteRoot, 'src', 'content', 'docs', 'reference'); +const outputDir = path.join(siteRoot, 'src', 'content', 'docs'); const descriptions = { - 'admin.md': 'Возможности админ-панели, управление пользователями, настройками, тарифами и поддержкой.', + 'index.md': 'Документация по запуску, настройке и сопровождению Telegram Mini App для Remnawave.', + 'getting-started/overview.md': 'Что входит в Remnawave Minishop и как связаны бот, Mini App, backend, worker и Remnawave Panel.', + 'getting-started/setup.md': 'Минимальный путь запуска Remnawave Minishop через Docker Compose.', + 'configuration/security.md': 'Секреты, публичные URL, доступ администраторов и базовые меры защиты Minishop.', + 'configuration/env-vars.md': 'Полный справочник переменных окружения Remnawave Minishop.', + 'features/core.md': 'Пользовательские и админские сценарии Remnawave Minishop.', + 'features/payments.md': 'Платежные провайдеры, кнопки оплаты и webhook-обработка.', + 'features/subscriptions.md': 'Period- и traffic-тарифы, premium-сквады, HWID-устройства и жизненный цикл подписки.', + 'features/tariffs.md': 'Каталог тарифов, period/traffic-модели, premium-сквады и HWID-устройства.', + 'features/web-app.md': 'Telegram Mini App, авторизация, публичные инструкции и проксирование.', + 'features/webapp-themes.md': 'Кастомные темы, CSS-токены, ассеты и пайплайн создания темы.', + 'features/admin-panel.md': 'Возможности админ-панели, управление пользователями, настройками, тарифами и поддержкой.', + 'features/support.md': 'Пользовательские тикеты, админский inbox, уведомления и лимиты поддержки.', + 'deploy-examples/index.md': 'Как выбрать готовый deploy example для production или проверки.', + 'deploy-examples/caddy.md': 'Запуск Remnawave Minishop с Caddy и автоматическим HTTPS.', + 'deploy-examples/nginx.md': 'Запуск Remnawave Minishop с Nginx и внешними TLS-сертификатами.', + 'deploy-examples/newt.md': 'Запуск через Pangolin/Newt без входящих портов на сервере приложения.', + 'deploy-examples/no-proxy.md': 'Прямой запуск backend и frontend портов без reverse proxy.', + 'migrations/index.md': 'Готовые сценарии миграции в Remnawave Minishop с других ботов.', + 'migrations/remnawave-tg-shop.md': 'Перенос данных со старого remnawave-tg-shop на split-архитектуру Minishop.', + 'payments/yookassa.md': 'Быстрый вход в настройку YooKassa для Remnawave Minishop.', + 'payments/freekassa.md': 'Быстрый вход в настройку FreeKassa для Remnawave Minishop.', + 'payments/platega.md': 'Настройка Platega, отдельных СБП/карта и crypto-кнопок.', + 'payments/severpay.md': 'Настройка SeverPay, MID, token, return URL и срока жизни ссылки.', + 'payments/wata.md': 'Настройка Wata, API token, TTL ссылки, подписи webhook и trusted IP.', + 'payments/cryptopay.md': 'Настройка CryptoPay, токена, сети, currency type и asset.', + 'payments/heleket.md': 'Настройка Heleket, merchant ID, payment API key, invoice currency и webhook-проверок.', + 'payments/telegram-stars.md': 'Оплата подписок и докупок через Telegram Stars.', + 'administration/users.md': 'Где управлять пользователями, подписками, блокировками и поддержкой.', + 'administration/maintenance.md': 'Обновления, миграции, резервные копии и проверки production-стека.', + 'troubleshooting/issues.md': 'Короткие чеклисты для частых проблем запуска, вебхуков, Mini App и платежей.', + 'troubleshooting/logs.md': 'Какие логи смотреть при диагностике backend, worker, frontend, миграций и вебхуков.', 'architecture.md': 'Краткая архитектура backend, frontend, worker и инфраструктурных сервисов.', 'configuration.md': 'Минимальный .env, bootstrap-секреты и настройка через Web App админку.', 'deployment.md': 'Docker Compose, reverse proxy, TLS, образы, обновления и резервные копии.', - 'env-vars.md': 'Полный справочник переменных окружения Remnawave Minishop.', - 'migration-to-minishop.md': 'Перенос данных со старого remnawave-tg-shop на split-архитектуру Minishop.', - 'support.md': 'Пользовательские тикеты, админский inbox, уведомления и лимиты поддержки.', - 'tariffs.md': 'Каталог тарифов, period/traffic-модели, premium-сквады и HWID-устройства.', - 'webapp.md': 'Telegram Mini App, авторизация, публичные инструкции и проксирование.', - 'webapp-themes.md': 'Кастомные темы, CSS-токены, ассеты и пайплайн создания темы.', }; const imageExtensions = new Set(['.avif', '.gif', '.jpeg', '.jpg', '.png', '.svg', '.webp']); @@ -26,23 +51,45 @@ function yamlString(value) { return JSON.stringify(value); } -function slugFor(fileName) { - return fileName.replace(/\.md$/i, ''); +function toPosix(relativePath) { + return relativePath.split(path.sep).join('/'); } -function extractTitle(fileName, content) { +function outputRelativePath(sourceRelativePath) { + if (sourceRelativePath === 'index.md') { + return 'index.md'; + } + if (!sourceRelativePath.includes('/')) { + return `reference/${sourceRelativePath}`; + } + return sourceRelativePath; +} + +function pagePathForSource(sourceRelativePath, hash = '') { + const output = outputRelativePath(sourceRelativePath).replace(/\.md$/i, ''); + const route = output === 'index' ? '/' : `/${output.replace(/\/index$/u, '')}/`; + return `${route}${hash}`; +} + +function titleForRelativePath(relativePath) { + const baseName = path.posix.basename(relativePath, '.md'); + return baseName; +} + +function extractTitle(relativePath, content) { const match = content.match(/^#\s+(.+?)\s*$/m); - return match?.[1] ?? slugFor(fileName); + return match?.[1] ?? titleForRelativePath(relativePath); } function stripFirstHeading(content) { return content.replace(/^#\s+.+?\s*\r?\n+/, ''); } -function rewriteMarkdownLinks(markdown) { +function rewriteMarkdownLinks(markdown, sourceRelativePath) { + const sourceDirectory = path.posix.dirname(sourceRelativePath); return markdown.replace(/\]\((?!https?:\/\/|mailto:|tel:|\/|#)([^)\s]+\.md)(#[^)]+)?\)/g, (match, target, hash = '') => { - const slug = slugFor(path.posix.basename(target)); - return `](/reference/${slug}/${hash})`; + const resolvedTarget = path.posix.normalize(path.posix.join(sourceDirectory, target)); + return `](${pagePathForSource(resolvedTarget, hash)})`; }); } @@ -52,45 +99,105 @@ function normalizeCodeFences(markdown) { .replace(/^```caddyfile\s*$/gim, '```txt'); } -function frontmatter({ title, description, fileName }) { - const editUrl = `https://gitlab.com/3252a8/remnawave-minshop/-/edit/main/docs/${encodeURIComponent(fileName)}`; +function extraFrontmatter(sourceRelativePath) { + if (sourceRelativePath !== 'index.md') { + return []; + } + + return [ + 'template: splash', + 'hero:', + ' tagline: "Telegram-бот и Mini App для продажи подписок Remnawave: платежи, тарифы, админка, поддержка и инструкции подключения."', + ' image:', + ' html: \'Интерфейс Remnawave Minishop\'', + ' actions:', + ' - text: "Быстрый старт"', + ' link: /getting-started/setup/', + ' icon: right-arrow', + ' - text: "Deploy examples"', + ' link: /deploy-examples/', + ' icon: setting', + ' variant: minimal', + ]; +} + +function frontmatter({ title, description, sourceRelativePath }) { + const editPath = sourceRelativePath + .split('/') + .map((segment) => encodeURIComponent(segment)) + .join('/'); + const editUrl = `https://gitlab.com/3252a8/remnawave-minshop/-/edit/main/docs/${editPath}`; return [ '---', `title: ${yamlString(title)}`, `description: ${yamlString(description)}`, `editUrl: ${yamlString(editUrl)}`, + ...extraFrontmatter(sourceRelativePath), '---', '', ].join('\n'); } -async function syncMarkdown(entries) { - for (const entry of entries.filter((item) => item.name.endsWith('.md'))) { - const sourcePath = path.join(sourceDir, entry.name); +async function walk(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + const absolutePath = path.join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...(await walk(absolutePath))); + continue; + } + if (entry.isFile()) { + files.push(absolutePath); + } + } + return files; +} + +async function syncMarkdown(files) { + for (const sourcePath of files.filter((file) => file.endsWith('.md'))) { + const sourceRelativePath = toPosix(path.relative(sourceDir, sourcePath)); + const outputRelative = outputRelativePath(sourceRelativePath); + const outputPath = path.join(outputDir, ...outputRelative.split('/')); const content = await readFile(sourcePath, 'utf8'); - const title = extractTitle(entry.name, content); - const body = normalizeCodeFences(rewriteMarkdownLinks(stripFirstHeading(content).trimStart())); + const title = extractTitle(sourceRelativePath, content); + const body = normalizeCodeFences( + rewriteMarkdownLinks(stripFirstHeading(content).trimStart(), sourceRelativePath), + ); const output = frontmatter({ title, - description: descriptions[entry.name] ?? title, - fileName: entry.name, + description: descriptions[sourceRelativePath] ?? title, + sourceRelativePath, }); - await writeFile(path.join(outputDir, entry.name), `${output}${body}\n`, 'utf8'); + await mkdir(path.dirname(outputPath), { recursive: true }); + await writeFile(outputPath, `${output}${body}\n`, 'utf8'); } } -async function syncImages(entries) { - for (const entry of entries.filter((item) => imageExtensions.has(path.extname(item.name).toLowerCase()))) { - await copyFile(path.join(sourceDir, entry.name), path.join(outputDir, entry.name)); +async function syncAssets(files) { + for (const sourcePath of files.filter((file) => imageExtensions.has(path.extname(file).toLowerCase()))) { + const sourceRelativePath = toPosix(path.relative(sourceDir, sourcePath)); + const outputRelative = !sourceRelativePath.includes('/') + ? sourceRelativePath + : sourceRelativePath; + const outputPath = path.join(outputDir, ...outputRelative.split('/')); + await mkdir(path.dirname(outputPath), { recursive: true }); + await copyFile(sourcePath, outputPath); + + if (!sourceRelativePath.includes('/')) { + const referenceOutputPath = path.join(outputDir, 'reference', sourceRelativePath); + await mkdir(path.dirname(referenceOutputPath), { recursive: true }); + await copyFile(sourcePath, referenceOutputPath); + } } } await rm(outputDir, { recursive: true, force: true }); await mkdir(outputDir, { recursive: true }); -const entries = await readdir(sourceDir, { withFileTypes: true }); -await syncMarkdown(entries); -await syncImages(entries); +const files = await walk(sourceDir); +await syncMarkdown(files); +await syncAssets(files); console.log(`Synced documentation from ${path.relative(repoRoot, sourceDir)} to ${path.relative(repoRoot, outputDir)}`); diff --git a/docs-site/src/assets/logo.svg b/docs-site/src/assets/logo.svg deleted file mode 100644 index cfd8eb4..0000000 --- a/docs-site/src/assets/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - Remnawave Minishop - Abstract layered wave mark. - - - - diff --git a/docs-site/src/content/docs/index.md b/docs-site/src/content/docs/index.md deleted file mode 100644 index 6874bac..0000000 --- a/docs-site/src/content/docs/index.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: "Remnawave Minishop" -description: "Документация по запуску, настройке и сопровождению Telegram Mini App для Remnawave." -template: splash -hero: - tagline: "Telegram-бот и Mini App для продажи подписок Remnawave: платежи, тарифы, админка, поддержка и инструкции подключения." - image: - alt: Интерфейс Remnawave Minishop - html: 'Интерфейс Remnawave Minishop' - actions: - - text: Быстрый старт - link: /reference/deployment/ - icon: right-arrow - - text: Настройка - link: /reference/configuration/ - icon: setting - variant: minimal ---- - -## Основные разделы - -- **Запуск и окружение** - минимальный `.env`, Docker Compose, reverse proxy, обновления и резервные копии. -- **Web App / Mini App** - Telegram-авторизация, email-вход, инструкции установки и публичные ссылки. -- **Админка и тарифы** - управление пользователями, платежами, темами, каталогом тарифов и premium-сквадами. -- **Миграция** - перенос со старого `remnawave-tg-shop` на текущую split-архитектуру. - -## Быстрые ссылки - -- [Развертывание](/reference/deployment/) -- [Переменные окружения](/reference/env-vars/) -- [Тарифы](/reference/tariffs/) -- [Темы Web App](/reference/webapp-themes/) diff --git a/docs-site/src/styles/custom.css b/docs-site/src/styles/custom.css index 71fdc73..ce05293 100644 --- a/docs-site/src/styles/custom.css +++ b/docs-site/src/styles/custom.css @@ -1,25 +1,73 @@ @layer starlight, nova, minishop; @layer minishop { - .site-title { + .site-title, + .site-title span { + color: #00fe7a; font-weight: 760; } + .site-title:hover, + .site-title:hover span { + color: #00fe7a; + } + .hero { gap: clamp(2rem, 6vw, 5rem); padding-block: clamp(3.5rem, 10vw, 6.5rem); } + .hero > .hero-html:has(.minishop-hero-screenshot) { + width: min(96vw, 72rem); + max-width: none; + } + .hero img { border: 1px solid var(--sl-color-gray-5); border-radius: 10px; box-shadow: 0 24px 70px rgb(15 23 42 / 18%); } + .minishop-hero-screenshot { + display: block; + width: 100%; + max-width: none; + height: auto; + } + :root[data-theme='dark'] .hero img { box-shadow: 0 24px 70px rgb(0 0 0 / 36%); } + @media (min-width: 50rem) { + .hero:has(.minishop-hero-screenshot) { + grid-template-columns: minmax(0, 1fr); + gap: clamp(2rem, 4vw, 4rem); + align-items: center; + } + + .hero > .hero-html:has(.minishop-hero-screenshot) { + order: 2; + width: min(100%, 90rem); + margin-inline: auto; + } + + .hero:has(.minishop-hero-screenshot) .stack { + align-items: center; + max-width: 64rem; + margin-inline: auto; + text-align: center; + } + + .hero:has(.minishop-hero-screenshot) .copy { + align-items: center; + } + + .hero:has(.minishop-hero-screenshot) .actions { + justify-content: center; + } + } + .sl-markdown-content :is(h2, h3) { letter-spacing: 0; } diff --git a/docs/administration/maintenance.md b/docs/administration/maintenance.md new file mode 100644 index 0000000..0340eab --- /dev/null +++ b/docs/administration/maintenance.md @@ -0,0 +1,27 @@ +# Обслуживание + +Плановое обслуживание обычно сводится к обновлению образов, проверке миграций, логов и резервных копий PostgreSQL. + +## Обновление + +```bash +docker compose pull +docker compose up -d +docker compose logs -f migrate backend worker +``` + +## Резервная копия PostgreSQL + +```bash +docker compose exec -T postgres sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB"' > backup.sql +``` + +## Проверки после работ + +- `docker compose ps` +- `docker compose logs -f backend worker frontend` +- `/healthz` на backend-домене +- вход в Mini App и админку +- тестовый платеж или тестовая активация + +Подробности: [развертывание](../deployment.md) и [логи](../troubleshooting/logs.md). diff --git a/docs/administration/users.md b/docs/administration/users.md new file mode 100644 index 0000000..7b322c2 --- /dev/null +++ b/docs/administration/users.md @@ -0,0 +1,19 @@ +# Пользователи + +Пользовательские операции выполняются в Web App админке. Доступ получают только Telegram-пользователи из `ADMIN_IDS`. + +## Что доступно администратору + +- список пользователей с поиском и фильтрами; +- просмотр подписки, статуса, трафика и premium-лимитов; +- блокировка пользователя; +- ручная синхронизация с Remnawave Panel; +- тикеты поддержки и ответы пользователю; +- просмотр платежей и служебных событий. + +## Связанные разделы + +- [Админ-панель](../features/admin-panel.md) +- [Поддержка](../features/support.md) +- [Тарифы](../features/tariffs.md) +- [Mini App](../features/web-app.md) diff --git a/docs/configuration.md b/docs/configuration.md index 72bc26f..1ecffe5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -7,7 +7,7 @@ Админка сохраняет overrides в базе данных и применяет их поверх `.env`. Это удобно для платежей, внешнего вида, поддержки, уведомлений, legacy-цен и большинства пользовательских параметров. Тарифы редактируются отдельно в разделе **Система -> Тарифы** и сохраняются в JSON-файл `TARIFFS_CONFIG_PATH`. -Полный справочник всех переменных вынесен в [env-vars.md](env-vars.md). +Полный справочник всех переменных вынесен в [configuration/env-vars.md](configuration/env-vars.md). ## Минимальный `.env` @@ -102,9 +102,9 @@ docker compose exec backend sh -lc 'id; touch /app/data/themes/test && rm /app/d ## Дополнительные разделы -- [env-vars.md](env-vars.md) - полный справочник переменных `.env`. -- [admin.md](admin.md) - как устроены overrides и allowlist настроек. -- [tariffs.md](tariffs.md) - JSON-каталог тарифов и редактор тарифов. -- [webapp.md](webapp.md) - домен Mini App, Telegram OAuth и email-вход. -- [support.md](support.md) - тикеты поддержки и уведомления. +- [configuration/env-vars.md](configuration/env-vars.md) - полный справочник переменных `.env`. +- [features/admin-panel.md](features/admin-panel.md) - как устроены overrides и allowlist настроек. +- [features/tariffs.md](features/tariffs.md) - JSON-каталог тарифов и редактор тарифов. +- [features/web-app.md](features/web-app.md) - домен Mini App, Telegram OAuth и email-вход. +- [features/support.md](features/support.md) - тикеты поддержки и уведомления. - [deployment.md](deployment.md) - Docker Compose, reverse proxy, Caddy/Nginx и обновления. diff --git a/docs/env-vars.md b/docs/configuration/env-vars.md similarity index 99% rename from docs/env-vars.md rename to docs/configuration/env-vars.md index 436ccf7..6c05bd7 100644 --- a/docs/env-vars.md +++ b/docs/configuration/env-vars.md @@ -368,7 +368,7 @@ PAYMENT_HELEKET_TELEGRAM_EMOJI ## Поддержка -Подробный сценарий описан в [support.md](support.md). +Подробный сценарий описан в [features/support.md](../features/support.md). | Переменная | Назначение | | --- | --- | diff --git a/docs/configuration/security.md b/docs/configuration/security.md new file mode 100644 index 0000000..f0efab5 --- /dev/null +++ b/docs/configuration/security.md @@ -0,0 +1,37 @@ +# Безопасность + +Безопасность Minishop в первую очередь держится на стабильных секретах, корректном разделении публичных доменов и ограниченном доступе к админке. + +## Секреты + +- `WEBAPP_SESSION_SECRET` должен быть постоянным между рестартами, иначе Web App-сессии станут невалидными. +- `WEBHOOK_SECRET_TOKEN` защищает Telegram webhook. +- `PANEL_WEBHOOK_SECRET` проверяет входящие события Remnawave Panel. +- Платежные токены и webhook-секреты храните в `.env` или настройках админки с учетом доступа к серверу. + +Сгенерировать секрет можно так: + +```bash +openssl rand -hex 32 +``` + +## Доступ администраторов + +- `ADMIN_IDS` задает Telegram ID администраторов. +- Админка доступна только пользователям из `ADMIN_IDS` при входе через Telegram. +- Email-only аккаунты не получают админский доступ. + +## Публичные URL + +- `WEBHOOK_BASE_URL` должен вести на backend webhook server. +- `SUBSCRIPTION_MINI_APP_URL` должен вести на frontend/Mini App. +- Не добавляйте `/api`, `/auth` или webhook-пути в `SUBSCRIPTION_MINI_APP_URL`. + +## Дополнительно + +- Используйте HTTPS на всех публичных доменах. +- Ограничивайте доступ к серверу и `.env`. +- Следите за логами платежных вебхуков и panel webhooks. +- После ротации секретов перезапускайте соответствующие сервисы и проверяйте вебхуки. + +См. также [переменные окружения](env-vars.md) и [развертывание](../deployment.md). diff --git a/docs/deploy-examples/caddy.md b/docs/deploy-examples/caddy.md new file mode 100644 index 0000000..003da0c --- /dev/null +++ b/docs/deploy-examples/caddy.md @@ -0,0 +1,39 @@ +# Caddy + +Вариант `deploy/examples/caddy` подходит, если нужен самый простой публичный HTTPS. Caddy сам выпускает и продлевает сертификаты Let's Encrypt. + +## Требования + +- На сервере открыты входящие `80/tcp` и `443/tcp`. +- DNS-записи `WEBHOOK_HOST` и `MINIAPP_HOST` смотрят на этот сервер. +- В `.env` заполнены домены, токены, секреты и доступы к Remnawave. + +## Запуск + +```bash +cd deploy/examples/caddy +cp .env.example .env +nano .env +docker compose up -d +``` + +Минимально поменяйте: + +- `WEBHOOK_HOST` и `MINIAPP_HOST`; +- `BOT_TOKEN`, `ADMIN_IDS`; +- `POSTGRES_PASSWORD`; +- `WEBAPP_SESSION_SECRET`, `WEBHOOK_SECRET_TOKEN`; +- `PANEL_API_URL`, `PANEL_API_KEY`, `PANEL_WEBHOOK_SECRET`. + +## Проверка + +```bash +docker compose ps +docker compose logs -f caddy backend worker frontend +``` + +Если нужна нестандартная логика Caddy, правьте `deploy/examples/caddy/Caddyfile` и перезапускайте: + +```bash +docker compose up -d --force-recreate caddy +``` diff --git a/docs/deploy-examples/index.md b/docs/deploy-examples/index.md new file mode 100644 index 0000000..94b9f17 --- /dev/null +++ b/docs/deploy-examples/index.md @@ -0,0 +1,39 @@ +# Deploy examples + +В `deploy/examples` лежат самодостаточные Compose-варианты для разных способов публикации Minishop. Каждый пример запускается из своей директории и содержит собственный `docker-compose.yml`, `.env.example` и README. + +```bash +cp .env.example .env +nano .env +docker compose up -d +``` + +После старта проверяйте: + +```bash +docker compose ps +docker compose logs -f backend worker frontend +``` + +## Какой вариант выбрать + +| Вариант | Когда использовать | Где лежит | +| --- | --- | --- | +| [Caddy](caddy.md) | Нужен самый простой публичный HTTPS с автоматическими сертификатами Let's Encrypt. | `deploy/examples/caddy` | +| [Nginx](nginx.md) | Уже используете Nginx и готовы положить TLS-сертификаты рядом с примером. | `deploy/examples/nginx` | +| [Pangolin/Newt](newt.md) | Публикуете сервисы через туннель без входящих портов на сервере приложения. | `deploy/examples/newt` | +| [No proxy](no-proxy.md) | Нужно напрямую открыть порты backend/frontend или проверить стек без reverse proxy. | `deploy/examples/no-proxy` | + +## Два публичных URL + +Для production обычно нужны два домена: + +- webhook/backend URL для Telegram, платежных систем и Remnawave webhooks; +- Mini App/frontend URL для Telegram Mini App, Web App и админки. + +Пример: + +```text +https://webhooks.example.com -> backend:8080 +https://app.example.com -> frontend:80 +``` diff --git a/docs/deploy-examples/newt.md b/docs/deploy-examples/newt.md new file mode 100644 index 0000000..b7a69be --- /dev/null +++ b/docs/deploy-examples/newt.md @@ -0,0 +1,38 @@ +# Pangolin / Newt + +Вариант `deploy/examples/newt` подходит, если сервер приложения не должен принимать входящие соединения. Newt подключается к Pangolin, а публичные домены настраиваются ресурсами в панели Pangolin. + +## Запуск + +```bash +cd deploy/examples/newt +cp .env.example .env +nano .env +docker compose up -d +``` + +В `.env` заполните: + +- `WEBHOOK_HOST` и `MINIAPP_HOST` - публичные домены ресурсов в Pangolin; +- `PANGOLIN_ENDPOINT`, `NEWT_ID`, `NEWT_SECRET` - значения из настроек site/client в Pangolin; +- обычные переменные приложения: `BOT_TOKEN`, `ADMIN_IDS`, `POSTGRES_PASSWORD`, секреты и доступ к Remnawave. + +## Ресурсы Pangolin + +Создайте два HTTP-ресурса для Newt site: + +| Публичный домен | Upstream | +| --- | --- | +| `https://webhooks.example.com` | `http://backend:8080` | +| `https://app.example.com` | `http://frontend:80` | + +Домены в Pangolin должны совпадать с `WEBHOOK_HOST` и `MINIAPP_HOST`. + +Официальная инструкция Pangolin по установке Newt site: . + +## Проверка + +```bash +docker compose ps +docker compose logs -f newt backend worker frontend +``` diff --git a/docs/deploy-examples/nginx.md b/docs/deploy-examples/nginx.md new file mode 100644 index 0000000..0555914 --- /dev/null +++ b/docs/deploy-examples/nginx.md @@ -0,0 +1,44 @@ +# Nginx + +Вариант `deploy/examples/nginx` поднимает Nginx в той же Docker-сети, что и приложение. Он подходит, если у вас уже есть TLS-сертификаты или нужен ручной контроль Nginx-конфига. + +## Маршрутизация + +- `WEBHOOK_HOST` проксируется в `backend:8080`. +- `MINIAPP_HOST` проксируется в `frontend:80`. +- `frontend` сам проксирует внутренние `/api`, `/auth` и ассеты тем в `backend:8081`. + +## Подготовка + +```bash +cd deploy/examples/nginx +cp .env.example .env +nano .env +``` + +Положите TLS-сертификаты в `ssl/`: + +```text +ssl/ + webhooks.example.com/ + fullchain.pem + privkey.pem + app.example.com/ + fullchain.pem + privkey.pem +``` + +Имена папок должны совпадать с `WEBHOOK_HOST` и `MINIAPP_HOST` в `.env`. + +## Запуск + +```bash +docker compose up -d +docker compose logs -f nginx backend worker frontend +``` + +Если нужно поменять заголовки, лимиты или TLS-настройки, правьте `deploy/examples/nginx/nginx.conf.template` и перезапускайте: + +```bash +docker compose up -d --force-recreate nginx +``` diff --git a/docs/deploy-examples/no-proxy.md b/docs/deploy-examples/no-proxy.md new file mode 100644 index 0000000..871b0b0 --- /dev/null +++ b/docs/deploy-examples/no-proxy.md @@ -0,0 +1,35 @@ +# Без reverse proxy + +Вариант `deploy/examples/no-proxy` напрямую публикует HTTP-порты backend и frontend. Он удобен для локальной проверки, внутренней сети или ситуации, когда HTTPS завершается внешней платформой. + +## Порты + +- backend/webhooks: `WEB_SERVER_BIND`, по умолчанию `0.0.0.0:8080`; +- frontend/Mini App: `FRONTEND_BIND`, по умолчанию `0.0.0.0:8082`. + +## Запуск + +```bash +cd deploy/examples/no-proxy +cp .env.example .env +nano .env +docker compose up -d +``` + +## Важно про HTTPS + +Контейнеры приложения сами не выпускают TLS-сертификаты. Для реального Telegram webhook и Mini App публичные URL должны быть HTTPS. + +Используйте этот вариант, если: + +- проверяете стек локально; +- публикуете сервисы только во внутренней сети; +- TLS уже завершается внешним reverse proxy, load balancer или платформой. + +## Проверка + +```bash +curl http://127.0.0.1:8080/healthz +curl http://127.0.0.1:8082/health +docker compose logs -f backend worker frontend +``` diff --git a/docs/deployment.md b/docs/deployment.md index 853a8f9..e59af1a 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,7 +1,7 @@ # Развертывание Документ описывает продакшен-запуск после разделения проекта на `backend`, `frontend` и `worker`. -Перед стартом заполните минимальный `.env` по [configuration.md](configuration.md). Полный справочник переменных лежит в [env-vars.md](env-vars.md); после первого входа большинство продуктовых настроек удобнее менять через Web App админку. +Перед стартом заполните минимальный `.env` по [configuration.md](configuration.md). Полный справочник переменных лежит в [configuration/env-vars.md](configuration/env-vars.md); после первого входа большинство продуктовых настроек удобнее менять через Web App админку. ## Быстрый старт @@ -27,16 +27,16 @@ docker compose logs -f backend worker frontend ## Готовые папки запуска -Для production удобнее использовать не корневой compose, а отдельные примеры в -[`deploy/examples`](../deploy/examples). В каждой папке лежат свой `docker-compose.yml`, -`.env.example`, README и нужный конфиг рядом: +Для production удобнее использовать не корневой compose, а отдельные примеры из +[Deploy examples](deploy-examples/index.md). В каждой папке лежат свой `docker-compose.yml`, +`.env.example` и нужный конфиг рядом, а подробные инструкции хранятся в `docs/`: | Папка | Назначение | Запуск | | --- | --- | --- | -| [`deploy/examples/caddy`](../deploy/examples/caddy) | Caddy с автоматическим HTTPS. | `cp .env.example .env`, заполнить `.env`, `docker compose up -d`. | -| [`deploy/examples/nginx`](../deploy/examples/nginx) | Nginx в Docker-сети приложения, TLS-сертификаты кладутся в `ssl/`. | `cp .env.example .env`, заполнить `.env`, положить сертификаты, `docker compose up -d`. | -| [`deploy/examples/newt`](../deploy/examples/newt) | Pangolin/Newt без входящих портов на сервере приложения. | `cp .env.example .env`, заполнить Newt credentials, создать ресурсы в Pangolin, `docker compose up -d`. | -| [`deploy/examples/no-proxy`](../deploy/examples/no-proxy) | Прямая публикация портов backend/frontend. | `cp .env.example .env`, заполнить публичные URL и порты, `docker compose up -d`. | +| [Caddy](deploy-examples/caddy.md) | Caddy с автоматическим HTTPS. | `cp .env.example .env`, заполнить `.env`, `docker compose up -d`. | +| [Nginx](deploy-examples/nginx.md) | Nginx в Docker-сети приложения, TLS-сертификаты кладутся в `ssl/`. | `cp .env.example .env`, заполнить `.env`, положить сертификаты, `docker compose up -d`. | +| [Pangolin/Newt](deploy-examples/newt.md) | Pangolin/Newt без входящих портов на сервере приложения. | `cp .env.example .env`, заполнить Newt credentials, создать ресурсы в Pangolin, `docker compose up -d`. | +| [No proxy](deploy-examples/no-proxy.md) | Прямая публикация портов backend/frontend. | `cp .env.example .env`, заполнить публичные URL и порты, `docker compose up -d`. | Пример для Caddy: @@ -84,7 +84,7 @@ docker compose logs migrate - `redis`: Redis 7 для FSM, кеша, rate-limit, очередей и locks. В production-примерах внешний доступ добавляют `caddy`, `nginx`, `newt` или прямые `ports` в -соответствующей папке из [`deploy/examples`](../deploy/examples). +соответствующем варианте из [Deploy examples](deploy-examples/index.md). ## Логи и проверка @@ -246,9 +246,9 @@ docker compose up -d backend worker Готовые reverse-proxy примеры лежат в: -- [`deploy/examples/caddy`](../deploy/examples/caddy) - Caddy, автоматический HTTPS; -- [`deploy/examples/nginx`](../deploy/examples/nginx) - Nginx, сертификаты кладутся рядом в `ssl/`; -- [`deploy/examples/newt`](../deploy/examples/newt) - Newt/Pangolin, без входящих портов на сервере приложения. +- [Caddy](deploy-examples/caddy.md) - автоматический HTTPS; +- [Nginx](deploy-examples/nginx.md) - сертификаты кладутся рядом в `ssl/`; +- [Newt/Pangolin](deploy-examples/newt.md) - без входящих портов на сервере приложения. Во всех вариантах схема одинаковая: @@ -274,7 +274,7 @@ app.example.com { ## Newt -Для Newt используйте [`deploy/examples/newt`](../deploy/examples/newt). В compose уже есть сервис +Для Newt используйте [Pangolin / Newt](deploy-examples/newt.md). В compose уже есть сервис `newt`, а в `.env.example` - поля `PANGOLIN_ENDPOINT`, `NEWT_ID` и `NEWT_SECRET`. В Pangolin создайте два HTTP-ресурса для этого Newt site: diff --git a/docs/admin.md b/docs/features/admin-panel.md similarity index 100% rename from docs/admin.md rename to docs/features/admin-panel.md diff --git a/docs/features/core.md b/docs/features/core.md new file mode 100644 index 0000000..a9ea65f --- /dev/null +++ b/docs/features/core.md @@ -0,0 +1,22 @@ +# Основные возможности + +Minishop закрывает путь от регистрации пользователя до оплаты, продления, поддержки и сопровождения подписки. + +## Для пользователей + +- Регистрация через Telegram Mini App или email-код. +- Просмотр подписки, срока действия, трафика и ссылки подключения. +- Покупка подписки, пакетов трафика и дополнительных устройств. +- Пробный период, промокоды и реферальные сценарии. +- Тикеты поддержки внутри Mini App. +- Встроенные инструкции установки и публичные ссылки `/s/`. + +## Для администраторов + +- Поиск и управление пользователями. +- Настройка платежей, тарифов, внешнего вида и поддержки. +- Рассылки, промокоды и логи действий. +- Ручная синхронизация с Remnawave Panel. +- Редактор JSON-каталога тарифов. + +Подробности: [админ-панель](admin-panel.md), [Mini App](web-app.md) и [поддержка](support.md). diff --git a/docs/features/payments.md b/docs/features/payments.md new file mode 100644 index 0000000..09f922a --- /dev/null +++ b/docs/features/payments.md @@ -0,0 +1,28 @@ +# Платежи + +Платежные методы включаются настройками и отображаются пользователю как кнопки оплаты в Mini App и Telegram-сценариях. + +## Поддерживаемые провайдеры + +- [YooKassa](../payments/yookassa.md) +- [FreeKassa](../payments/freekassa.md) +- [Platega](../payments/platega.md) +- [SeverPay](../payments/severpay.md) +- [Wata](../payments/wata.md) +- [CryptoPay](../payments/cryptopay.md) +- [Heleket](../payments/heleket.md) +- [Telegram Stars](../payments/telegram-stars.md) + +## Типовой порядок настройки + +1. Включите нужный провайдер в админке или через `.env`. +2. Заполните публичные параметры и секреты. +3. Настройте webhook URL у провайдера, если это требуется. +4. Проверьте порядок и подписи кнопок оплаты. +5. Выполните тестовый платеж и проверьте логи backend. + +## Где смотреть параметры + +- [Справочник `.env`](../configuration/env-vars.md) содержит все ключи провайдеров. +- [Админ-панель](admin-panel.md) описывает UI-настройки платежей. +- [Тарифы](tariffs.md) описывают цены, Stars и сценарии покупки. diff --git a/docs/features/subscriptions.md b/docs/features/subscriptions.md new file mode 100644 index 0000000..1739c6f --- /dev/null +++ b/docs/features/subscriptions.md @@ -0,0 +1,21 @@ +# Подписки + +Подписки управляются через каталог тарифов и синхронизируются с Remnawave Panel. + +## Модели тарифов + +- **Period** - подписка на срок с месячным лимитом трафика. +- **Traffic** - покупка пакетов трафика без привязки к периоду. +- **Premium** - отдельные premium-сквады и premium-лимит. +- **HWID-устройства** - докупка дополнительных устройств при включенном разделе устройств. + +## Жизненный цикл + +- создание пользователя в панели; +- применение пробного периода или покупки; +- продление и докупки; +- предупреждения по трафику; +- синхронизация подписки и статусов; +- обработка смены тарифа. + +Подробности: [тарифы](tariffs.md) и [Mini App](web-app.md). diff --git a/docs/support.md b/docs/features/support.md similarity index 96% rename from docs/support.md rename to docs/features/support.md index a57effd..94b8e91 100644 --- a/docs/support.md +++ b/docs/features/support.md @@ -64,7 +64,7 @@ Email-уведомления администраторам включаются | `SUPPORT_ADMIN_NOTIFICATION_COOLDOWN_SECONDS` | Минимальная пауза между повторными Telegram/log уведомлениями по одному непрочитанному тикету. | | `SUPPORT_ADMIN_EMAIL_COOLDOWN_SECONDS` | Минимальная пауза между повторными email-уведомлениями по одному непрочитанному тикету. | -Все эти параметры описаны в [env-vars.md](env-vars.md). Основной рекомендуемый способ менять их - админка **Система -> Настройки -> Поддержка**; значения применяются как override поверх `.env`. +Все эти параметры описаны в [env-vars.md](../configuration/env-vars.md). Основной рекомендуемый способ менять их - админка **Система -> Настройки -> Поддержка**; значения применяются как override поверх `.env`. ## API и хранение diff --git a/docs/tariffs.md b/docs/features/tariffs.md similarity index 99% rename from docs/tariffs.md rename to docs/features/tariffs.md index b85ca81..0930ce3 100644 --- a/docs/tariffs.md +++ b/docs/features/tariffs.md @@ -5,7 +5,7 @@ - JSON-каталог тарифов из `TARIFFS_CONFIG_PATH` (по умолчанию `data/tariffs.json`); - конфигурация через переменные `.env`, если JSON-файл отсутствует. -JSON-каталог может содержать несколько тарифов разных моделей: подписки на срок, пакеты трафика без срока действия, разные наборы Internal Squads, лимиты устройств и пакеты докупки. Пример формата: [data/tariffs.example.json](../data/tariffs.example.json). +JSON-каталог может содержать несколько тарифов разных моделей: подписки на срок, пакеты трафика без срока действия, разные наборы Internal Squads, лимиты устройств и пакеты докупки. Пример формата: [data/tariffs.example.json](https://gitlab.com/3252a8/remnawave-minshop/-/blob/main/data/tariffs.example.json). Коротко по моделям: @@ -30,7 +30,7 @@ JSON-каталог может содержать несколько тариф После сохранения изменения применяются к новым запросам Web App сразу, потому что конфиг тарифов загружается из JSON при обращении. Уже созданные подписки сохраняют свой `tariff_key`; при удалении или отключении тарифа проверьте, что активные подписки с этим ключом не требуют дальнейшего продления или смены. -Подробности по админ-панели, правам доступа, сохранению настроек и списку разделов есть в [admin.md](admin.md). +Подробности по админ-панели, правам доступа, сохранению настроек и списку разделов есть в [админ-панели](admin-panel.md). ## Как выбирается режим diff --git a/docs/webapp.md b/docs/features/web-app.md similarity index 94% rename from docs/webapp.md rename to docs/features/web-app.md index ebf4eea..a97a4c7 100644 --- a/docs/webapp.md +++ b/docs/features/web-app.md @@ -16,7 +16,7 @@ Web App собирается в отдельный `frontend` image и отда - реферальную ссылку и статистику приглашений; - привязку email и Telegram к одному аккаунту. -Для администраторов из `ADMIN_IDS` Web App также показывает админ-панель: статистику, **пользователей** (поиск, фильтры, premium-трафик), поддержку, рассылки, промокоды, логи, настройки и редактор тарифов. Подробности: [admin.md](admin.md). +Для администраторов из `ADMIN_IDS` Web App также показывает админ-панель: статистику, **пользователей** (поиск, фильтры, premium-трафик), поддержку, рассылки, промокоды, логи, настройки и редактор тарифов. Подробности: [админ-панель](admin-panel.md). ## Настройки `.env` @@ -118,7 +118,7 @@ Email-вход работает через одноразовый код: Для Brevo обычно подходит порт `587` с STARTTLS. Если основной порт недоступен, приложение пробует порты из `SMTP_FALLBACK_PORTS`; порт `465` используется через SSL. -Полный список переменных, обязательные поля для включения email-входа и типичные ошибки подключения описаны в разделе **SMTP и вход по email** в [configuration.md](configuration.md). +Полный список переменных, обязательные поля для включения email-входа и типичные ошибки подключения описаны в разделе **SMTP и вход по email** в [configuration.md](../configuration.md). ## Проксирование @@ -131,12 +131,12 @@ Email-вход работает через одноразовый код: WebApp API на `backend:8081`, поэтому внешний reverse proxy обычно не должен отправлять эти пути в `backend:8081` напрямую. -Готовые примеры лежат в [`deploy/examples`](../deploy/examples): +Готовые варианты описаны в [Deploy examples](../deploy-examples/index.md): -- `caddy` - Caddy с автоматическим HTTPS; -- `nginx` - Nginx с сертификатами в соседней папке `ssl/`; -- `newt` - Pangolin/Newt; -- `no-proxy` - прямая публикация портов для проверки или внешней TLS-платформы. +- [Caddy](../deploy-examples/caddy.md) - автоматический HTTPS; +- [Nginx](../deploy-examples/nginx.md) - сертификаты в соседней папке `ssl/`; +- [Pangolin/Newt](../deploy-examples/newt.md) - публикация без входящих портов на сервере приложения; +- [No proxy](../deploy-examples/no-proxy.md) - прямая публикация портов для проверки или внешней TLS-платформы. В default `docker-compose.yml` наружу публикуются `frontend` и webhook/backend port, а внутри Docker network сервисы доступны друг другу по service DNS names: diff --git a/docs/webapp-themes.md b/docs/features/webapp-themes.md similarity index 100% rename from docs/webapp-themes.md rename to docs/features/webapp-themes.md diff --git a/docs/webapp-themes.webp b/docs/features/webapp-themes.webp similarity index 100% rename from docs/webapp-themes.webp rename to docs/features/webapp-themes.webp diff --git a/docs/getting-started/overview.md b/docs/getting-started/overview.md new file mode 100644 index 0000000..6470c14 --- /dev/null +++ b/docs/getting-started/overview.md @@ -0,0 +1,26 @@ +# Обзор + +Remnawave Minishop состоит из Telegram-бота, backend API, worker-процессов, frontend/Mini App и инфраструктурных сервисов PostgreSQL и Redis. В production эти части запускаются через Docker Compose и общаются с Remnawave Panel по API и вебхукам. + +## Основные компоненты + +- **Backend** - Telegram webhook, платежные вебхуки, panel webhooks, API для Mini App и админки. +- **Worker** - фоновые задачи, синхронизация подписок, обработка очереди вебхуков и тарифных событий. +- **Frontend** - отдельный nginx-образ с Mini App и админкой. +- **PostgreSQL** - пользователи, платежи, настройки, поддержка, промокоды и служебные данные. +- **Redis** - FSM, кеши, rate limit, очередь вебхуков и distributed locks. + +## Сценарии + +- пользователь открывает Mini App, видит подписку и оплачивает тариф; +- платежный провайдер отправляет webhook в backend; +- worker применяет фоновые задачи и синхронизацию; +- Remnawave Panel хранит пользователя, подписку и ссылку подключения; +- администратор управляет тарифами, поддержкой, пользователями и настройками через админку. + +## Куда идти дальше + +- [Установка](setup.md) - базовый запуск через Compose. +- [Deploy examples](../deploy-examples/index.md) - готовые варианты публикации. +- [Архитектура](../architecture.md) - структура каталогов и сервисов. +- [Mini App](../features/web-app.md) - публичный frontend, Telegram OAuth и инструкции установки. diff --git a/docs/getting-started/setup.md b/docs/getting-started/setup.md new file mode 100644 index 0000000..ee034d0 --- /dev/null +++ b/docs/getting-started/setup.md @@ -0,0 +1,38 @@ +# Установка + +Начните с `.env`, затем поднимите Compose-стек и проверьте backend, worker и frontend. + +## Минимальный запуск + +```bash +cp .env.example .env +nano .env +docker compose up -d --build +docker compose ps +docker compose logs -f backend worker frontend +``` + +## Что заполнить в первую очередь + +- `BOT_TOKEN` и `ADMIN_IDS` для доступа к боту и админке. +- `WEBHOOK_BASE_URL` для Telegram, платежных и panel webhook URL. +- `SUBSCRIPTION_MINI_APP_URL` для Mini App и кнопок в Telegram. +- `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`. +- `WEBAPP_SESSION_SECRET`, `WEBHOOK_SECRET_TOKEN`, `PANEL_API_URL`, `PANEL_API_KEY`, `PANEL_WEBHOOK_SECRET`. + +## Как выбрать Compose-вариант + +- Для быстрого публичного HTTPS берите [Caddy](../deploy-examples/caddy.md). +- Если у вас уже есть TLS-сертификаты и нужен Nginx в Docker-сети, берите [Nginx](../deploy-examples/nginx.md). +- Если нельзя открывать входящие порты на сервере приложения, берите [Pangolin/Newt](../deploy-examples/newt.md). +- Для локальной проверки или внешнего TLS-терминатора берите [no-proxy](../deploy-examples/no-proxy.md). + +## После первого входа + +1. Откройте админку через Mini App. +2. Проверьте платежные методы в настройках. +3. Настройте каталог тарифов. +4. Проверьте инструкции подключения. +5. Сделайте тестовую покупку или пробную активацию. + +Подробности: [настройка окружения](../configuration.md) и [развертывание](../deployment.md). diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..b852c2c --- /dev/null +++ b/docs/index.md @@ -0,0 +1,32 @@ +# Remnawave Minishop + +Remnawave Minishop - Telegram-бот и Mini App для продажи и управления подписками Remnawave. Документация помогает развернуть стек, настроить платежи, тарифы, админку, поддержку и публичный личный кабинет. + +> Проект работает вместе с Remnawave Panel: панель хранит пользователей и подписки, а Minishop отвечает за Telegram, платежи, Mini App, тарифы и операционную админку. + +## Быстрый старт + +- [Обзор](getting-started/overview.md) - архитектура, сервисы и основные сценарии. +- [Установка](getting-started/setup.md) - путь от `.env` до первого запуска. +- [Deploy examples](deploy-examples/index.md) - Caddy, Nginx, Pangolin/Newt и no-proxy варианты. +- [Настройка платежей](features/payments.md) - включение провайдеров и проверка вебхуков. +- [Безопасность](configuration/security.md) - секреты, доступы и публичные URL. +- [Админ-панель](features/admin-panel.md) - пользователи, настройки, рассылки, поддержка и тарифы. +- [Миграции](migrations/index.md) - готовые сценарии переноса с других ботов. +- [Устранение неполадок](troubleshooting/issues.md) - быстрые проверки для частых проблем. + +## Ключевые возможности + +- **Продажа подписок** - period- и traffic-тарифы, докупки трафика, HWID-устройства, premium-сквады и Telegram Stars. +- **Жизненный цикл пользователей** - регистрация, пробный период, продление, синхронизация с панелью и предупреждения по трафику. +- **Mini App** - личный кабинет, инструкции установки, Telegram OAuth, email-вход и публичные referral-ссылки. +- **Операционные инструменты** - админка, тикеты поддержки, промокоды, рассылки, логи и настройки поверх `.env`. + +## Справочник + +- [Переменные окружения](configuration/env-vars.md) +- [Развертывание](deployment.md) +- [Тарифы](features/tariffs.md) +- [Темы Web App](features/webapp-themes.md) +- [Миграции](migrations/index.md) +- [Миграция с remnawave-tg-shop](migrations/remnawave-tg-shop.md) diff --git a/docs/migrations/index.md b/docs/migrations/index.md new file mode 100644 index 0000000..36e2add --- /dev/null +++ b/docs/migrations/index.md @@ -0,0 +1,27 @@ +# Миграции с других ботов + +Этот раздел содержит готовые инструкции миграции в Remnawave Minishop из уже описанных источников. Каждая поддерживаемая миграция должна быть отдельным Markdown-файлом с конкретными шагами, ограничениями, командами и проверками. + +Сейчас в документации есть только один готовый сценарий: + +| Источник | Поддерживаемый случай | Инструкция | +| --- | --- | --- | +| `remnawave-tg-shop` `v2.7.0` и близкие версии | Переезд старого stack/volume PostgreSQL на split-архитектуру Minishop `v3.4+`, обновление `.env`, запуск `migrate`, проверка reverse proxy | [Миграция с remnawave-tg-shop](remnawave-tg-shop.md) | + +## Что покрывает текущая миграция + +Инструкция для `remnawave-tg-shop` рассчитана на родственный стек, где заранее известны Docker volumes, контейнеры, схема БД и путь обновления: + +- перенос PostgreSQL volume `remnawave-tg-shop-db-data` в новый volume Minishop; +- создание новых пустых volumes `redis-data` и `shop-data`; +- перенос Caddy volumes при использовании Caddy-варианта; +- обновление переменных окружения, которые изменились после `v2.7.0`; +- запуск one-shot сервиса `migrate`; +- переход с одного upstream `remnawave-tg-shop:8000` на `backend:8080` и `frontend:80`; +- запуск через корневой compose или готовые deploy examples. + +## Что пока не описано + +Для других Telegram-ботов, самописных панелей и ручных таблиц готовой инструкции пока нет. Такие источники нельзя переносить по инструкции `remnawave-tg-shop`: у них могут отличаться таблицы пользователей, модель тарифов, статусы платежей, связь с Remnawave Panel, формат промокодов, рефералы и правила отката. + +Когда для конкретного источника появится проверенный сценарий, он должен быть добавлен в этот раздел отдельным файлом и отдельной строкой в таблице выше. diff --git a/docs/migration-to-minishop.md b/docs/migrations/remnawave-tg-shop.md similarity index 94% rename from docs/migration-to-minishop.md rename to docs/migrations/remnawave-tg-shop.md index a5ee041..f2b54ed 100644 --- a/docs/migration-to-minishop.md +++ b/docs/migrations/remnawave-tg-shop.md @@ -1,5 +1,9 @@ # Миграция с `remnawave-tg-shop` (≤ v2.7.0) на `remnawave-minishop` (v3.4+) +Эта страница - готовый сценарий для legacy-стека `remnawave-tg-shop`. Это единственная миграция с другого бота, которая сейчас описана в документации. Для других Telegram-ботов, самописных панелей и ручных таблиц готового сценария пока нет: их нельзя переносить по этой инструкции без отдельного анализа схемы БД, тарифов, платежей и связи с Remnawave Panel. + +Автоматический скрипт ниже рассчитан именно на родственный стек `remnawave-tg-shop`, где структура БД и Docker volumes известны заранее. Для других ботов нужен отдельный адаптер экспорта/импорта. + ## Короткий путь без смены ветки и сборки Если вы используете только готовые Docker-образы и не собираете проект @@ -127,7 +131,7 @@ docker compose \ | — | `REDIS_URL=redis://redis:6379/0` | Обязательна для воркера, очередей и rate-limit. По умолчанию в compose-файлах уже задана. | | — | `WEBAPP_SESSION_SECRET`, `WEBAPP_ENABLED`, `WEBAPP_SERVER_PORT`, `WEBAPP_THEMES_DIR`, `TARIFFS_CONFIG_PATH` | Новые настройки Web App / тарифного каталога. Безопасные дефолты есть в `.env.example`. | -Полный референс — [docs/configuration.md](configuration.md). Скрипт миграции +Полный референс — [docs/configuration.md](../configuration.md). Скрипт миграции эти переменные **не правит** автоматически (только `POSTGRES_HOST`), потому что у каждой инсталляции свой шаблон `.env` с кастомными значениями. Лучше сравнить свой `.env` с `.env.example` глазами один раз, чем получить @@ -357,8 +361,8 @@ server { ``` Полные примеры (Caddy, Nginx, Newt/Pangolin и запуск без reverse proxy) — в -[docs/deployment.md](deployment.md), [docs/webapp.md](webapp.md) и папке -[`deploy/examples`](../deploy/examples). Если раньше прокси указывал на +[docs/deployment.md](../deployment.md), [docs/features/web-app.md](../features/web-app.md) и +[Deploy examples](../deploy-examples/index.md). Если раньше прокси указывал на `remnawave-tg-shop:8000` напрямую, после миграции нужно либо переключиться на `backend:8080` / `frontend:80`, либо использовать готовый Caddy/Nginx/Newt пример, который уже знает правильную маршрутизацию. diff --git a/docs/payments/cryptopay.md b/docs/payments/cryptopay.md new file mode 100644 index 0000000..2cb2235 --- /dev/null +++ b/docs/payments/cryptopay.md @@ -0,0 +1,28 @@ +# CryptoPay + +CryptoPay используется для криптовалютных платежей через отдельный токен и сеть Crypto Bot API. + +## Что включить + +- `CRYPTOPAY_ENABLED` - включает CryptoPay среди доступных методов. +- Presentation-ключи `PAYMENT_CRYPTOPAY_*` - подписи и иконки кнопки в Mini App и Telegram. + +## Что настроить + +1. Укажите `CRYPTOPAY_TOKEN`. +2. Выберите `CRYPTOPAY_NETWORK`: `mainnet` или `testnet`. +3. Задайте `CRYPTOPAY_CURRENCY_TYPE`: `fiat` или `crypto`. +4. Проверьте `CRYPTOPAY_ASSET`, например `RUB`, `USDT` или `BTC`. +5. Добавьте `cryptopay` в `PAYMENT_METHODS_ORDER`. + +## Проверка + +- Для тестов используйте соответствующую сеть: testnet-токен не должен попадать в mainnet-настройки. +- Выполните тестовый платеж и проверьте, что статус закрывается после callback от провайдера. +- Если сумма или asset выглядят неверно, проверьте сочетание `CRYPTOPAY_CURRENCY_TYPE` и `CRYPTOPAY_ASSET`. + +## Где подробнее + +- [Переменные CryptoPay](../configuration/env-vars.md#cryptopay) +- [Настройка платежей](../features/payments.md) +- [Логи и диагностика](../troubleshooting/logs.md) diff --git a/docs/payments/freekassa.md b/docs/payments/freekassa.md new file mode 100644 index 0000000..9c8d020 --- /dev/null +++ b/docs/payments/freekassa.md @@ -0,0 +1,16 @@ +# FreeKassa + +FreeKassa подключается как отдельный платежный метод и обрабатывает входящие webhook-события через backend. + +## Что настроить + +- Включение провайдера: `FREEKASSA_ENABLED`. +- ID магазина, API/secret-ключи и настройки подписи. +- Trusted IP allowlist, если используется. +- Публичный webhook URL на `WEBHOOK_BASE_URL`. + +## Где подробнее + +- [Переменные FreeKassa](../configuration/env-vars.md#freekassa) +- [Платежи](../features/payments.md) +- [Логи и проверка](../troubleshooting/logs.md) diff --git a/docs/payments/heleket.md b/docs/payments/heleket.md new file mode 100644 index 0000000..3fc183f --- /dev/null +++ b/docs/payments/heleket.md @@ -0,0 +1,31 @@ +# Heleket + +Heleket используется для crypto-инвойсов с отдельными merchant ID, payment API key, валютой инвойса и настройками webhook-проверки. + +## Что включить + +- `HELEKET_ENABLED` - включает Heleket среди доступных методов. +- Presentation-ключи `PAYMENT_HELEKET_*` - подписи и иконки кнопки. + +## Что настроить + +1. Укажите `HELEKET_BASE_URL`, `HELEKET_MERCHANT_ID` и `HELEKET_API_KEY`. +2. Настройте `HELEKET_CURRENCY`. +3. При необходимости задайте `HELEKET_TO_CURRENCY` и `HELEKET_NETWORK`. +4. Проверьте `HELEKET_RETURN_URL` и `HELEKET_SUCCESS_URL`. +5. Настройте `HELEKET_LIFETIME_SECONDS`: допустимый диапазон 300..43200. +6. Если включаете проверку webhook, задайте `HELEKET_VERIFY_WEBHOOK_SIGNATURE`. +7. Для IP-фильтрации заполните `HELEKET_TRUSTED_IPS`. +8. Добавьте `heleket` в `PAYMENT_METHODS_ORDER`. + +## Проверка + +- Создайте тестовый инвойс и убедитесь, что пользователь получает корректную ссылку. +- Проверьте, что сеть и валюта соответствуют настройкам в кабинете Heleket. +- Если webhook отклоняется, проверьте подпись, allowlist и фактический payload в backend-логах. + +## Где подробнее + +- [Переменные Heleket](../configuration/env-vars.md#heleket) +- [Настройка платежей](../features/payments.md) +- [Логи и диагностика](../troubleshooting/logs.md) diff --git a/docs/payments/platega.md b/docs/payments/platega.md new file mode 100644 index 0000000..faab142 --- /dev/null +++ b/docs/payments/platega.md @@ -0,0 +1,30 @@ +# Platega + +Platega подключается как отдельный платежный провайдер, но внутри Minishop может дать несколько кнопок: основную legacy-кнопку, СБП/карту и крипто-кнопку. Общие merchant-параметры задаются один раз, а method ID и подписи кнопок настраиваются отдельно. + +## Что включить + +- `PLATEGA_ENABLED` - общий флаг провайдера. +- `PLATEGA_SBP_ENABLED` - отдельная кнопка СБП/карта. +- `PLATEGA_CRYPTO_ENABLED` - отдельная crypto-кнопка Platega. +- `PLATEGA_PAYMENT_METHOD` - legacy/fallback method ID для старых callback и старых установок. + +## Что настроить + +1. Укажите `PLATEGA_BASE_URL`, `PLATEGA_MERCHANT_ID` и `PLATEGA_SECRET`. +2. Заполните `PLATEGA_SBP_METHOD` и/или `PLATEGA_CRYPTO_METHOD`, если используете отдельные кнопки. +3. Проверьте `PLATEGA_RETURN_URL` и `PLATEGA_FAILED_URL`. +4. Настройте тексты и иконки кнопок через `PAYMENT_PLATEGA_SBP_*` и `PAYMENT_PLATEGA_CRYPTO_*`. +5. Добавьте нужные методы в `PAYMENT_METHODS_ORDER`. + +## Проверка + +- После сохранения настроек откройте Mini App и убедитесь, что видны только включенные Platega-кнопки. +- Выполните тестовую оплату для каждой включенной кнопки: СБП/карта и crypto используют разные method ID. +- При ошибках проверьте backend-логи и ответ провайдера при создании платежной ссылки. + +## Где подробнее + +- [Переменные Platega](../configuration/env-vars.md#platega) +- [Настройка платежей](../features/payments.md) +- [Логи и диагностика](../troubleshooting/logs.md) diff --git a/docs/payments/severpay.md b/docs/payments/severpay.md new file mode 100644 index 0000000..cd716fd --- /dev/null +++ b/docs/payments/severpay.md @@ -0,0 +1,28 @@ +# SeverPay + +SeverPay подключается как отдельный платежный метод с собственным MID, token и сроком жизни платежной ссылки. + +## Что включить + +- `SEVERPAY_ENABLED` - показывает SeverPay среди доступных методов оплаты. +- Presentation-ключи `PAYMENT_SEVERPAY_*` - подписи и иконки кнопки в Mini App и Telegram. + +## Что настроить + +1. Укажите `SEVERPAY_BASE_URL`. +2. Заполните `SEVERPAY_MID` и `SEVERPAY_TOKEN`. +3. Настройте `SEVERPAY_RETURN_URL`. +4. При необходимости задайте `SEVERPAY_LIFETIME_MINUTES`. +5. Добавьте `severpay` в `PAYMENT_METHODS_ORDER`. + +## Проверка + +- Создайте тестовый платеж и проверьте, что пользователь получает платежную ссылку. +- Убедитесь, что ссылка живет ожидаемое время, если задан `SEVERPAY_LIFETIME_MINUTES`. +- После оплаты проверьте статус платежа в backend-логах и в админке. + +## Где подробнее + +- [Переменные SeverPay](../configuration/env-vars.md#severpay) +- [Настройка платежей](../features/payments.md) +- [Логи и диагностика](../troubleshooting/logs.md) diff --git a/docs/payments/telegram-stars.md b/docs/payments/telegram-stars.md new file mode 100644 index 0000000..a14907e --- /dev/null +++ b/docs/payments/telegram-stars.md @@ -0,0 +1,19 @@ +# Telegram Stars + +Telegram Stars используются напрямую и поддерживаются в legacy-ценах и JSON-каталоге тарифов. + +## Где применяются Stars + +- Цены периодов подписки. +- Пакеты трафика. +- Premium-докупки. +- HWID-докупки, если они включены в каталоге тарифов. + +## Что проверить + +- `STARS_ENABLED`. +- Stars-цены в legacy-настройках или JSON-каталоге. +- Корректное округление цены до целого количества Stars. +- Сценарии смены тарифа: XTR/Stars-докупки не конвертируются без явного курса. + +Подробности: [переменные платежей](../configuration/env-vars.md#платежи) и [тарифы](../features/tariffs.md). diff --git a/docs/payments/wata.md b/docs/payments/wata.md new file mode 100644 index 0000000..777849e --- /dev/null +++ b/docs/payments/wata.md @@ -0,0 +1,30 @@ +# Wata + +Wata подключается как отдельный провайдер с bearer token, платежными ссылками и опциональной проверкой подписи webhook. + +## Что включить + +- `WATA_ENABLED` - включает Wata для пользователей. +- `WATA_ADMIN_ONLY_ENABLED` - оставляет метод доступным только для админских сценариев, если используется вместо публичного включения. +- Presentation-ключи `PAYMENT_WATA_*` - подписи и иконки кнопки. + +## Что настроить + +1. Укажите `WATA_BASE_URL` и `WATA_API_TOKEN`. +2. Проверьте `WATA_RETURN_URL` и `WATA_FAILED_URL`. +3. Настройте `WATA_LINK_TTL_MINUTES`: минимум 15 минут, максимум 43200. +4. Если включаете проверку подписи, задайте `WATA_WEBHOOK_VERIFY_SIGNATURE` и при необходимости `WATA_PUBLIC_KEY`. +5. Для дополнительной защиты заполните `WATA_TRUSTED_IPS`. +6. Добавьте `wata` в `PAYMENT_METHODS_ORDER`. + +## Проверка + +- Создайте тестовый платеж и убедитесь, что ссылка открывается у пользователя. +- Проверьте входящий webhook: подпись и IP-allowlist должны соответствовать фактическому запросу Wata. +- Если платеж остается в pending, проверьте backend-логи вокруг webhook и статуса ссылки. + +## Где подробнее + +- [Переменные Wata](../configuration/env-vars.md#wata) +- [Настройка платежей](../features/payments.md) +- [Логи и диагностика](../troubleshooting/logs.md) diff --git a/docs/payments/yookassa.md b/docs/payments/yookassa.md new file mode 100644 index 0000000..ef94066 --- /dev/null +++ b/docs/payments/yookassa.md @@ -0,0 +1,16 @@ +# YooKassa + +YooKassa используется для рублевых оплат и может участвовать в сценариях автопродления period-подписок. + +## Что настроить + +- Включение провайдера: `YOOKASSA_ENABLED`. +- Идентификаторы и секреты магазина. +- Webhook URL на backend-домен. +- Отображение кнопки оплаты и порядок платежных методов. + +## Где подробнее + +- [Переменные YooKassa](../configuration/env-vars.md#yookassa) +- [Платежи](../features/payments.md) +- [Тарифы и автопродление](../features/tariffs.md#автопродление-пробный-период-и-бонусы) diff --git a/docs/troubleshooting/issues.md b/docs/troubleshooting/issues.md new file mode 100644 index 0000000..de3aae9 --- /dev/null +++ b/docs/troubleshooting/issues.md @@ -0,0 +1,33 @@ +# Проблемы + +Начинайте диагностику с состояния контейнеров и логов, затем проверяйте публичные URL и секреты. + +## Стек не стартует + +- Проверьте `docker compose ps`. +- Посмотрите `docker compose logs migrate`. +- Убедитесь, что PostgreSQL и Redis здоровы. +- Проверьте обязательные переменные в `.env`. + +## Telegram webhook не работает + +- Проверьте `WEBHOOK_BASE_URL`. +- Убедитесь, что домен доступен по HTTPS. +- Проверьте `WEBHOOK_SECRET_TOKEN`. +- Посмотрите backend-логи на момент входящего события. + +## Mini App не открывается + +- Проверьте `SUBSCRIPTION_MINI_APP_URL`. +- Убедитесь, что URL указывает на frontend, а не на `/api` или webhook-домен. +- Проверьте настройки BotFather. +- Посмотрите frontend и backend-логи. + +## Платеж не засчитался + +- Проверьте включение провайдера. +- Проверьте webhook URL и секреты. +- Посмотрите backend-логи. +- Сверьте статус платежа в админке и кабинете провайдера. + +Подробности: [логи](logs.md) и [развертывание](../deployment.md). diff --git a/docs/troubleshooting/logs.md b/docs/troubleshooting/logs.md new file mode 100644 index 0000000..70f253e --- /dev/null +++ b/docs/troubleshooting/logs.md @@ -0,0 +1,104 @@ +# Логи + +Логи - главный источник диагностики при проблемах запуска, платежей, вебхуков и синхронизации с Remnawave Panel. + +## Основные команды + +```bash +docker compose logs -f backend +docker compose logs -f worker +docker compose logs -f frontend +docker compose logs migrate +``` + +## Что искать + +- ошибки миграций в `migrate`; +- ошибки Telegram webhook и payment webhook в `backend`; +- проблемы очереди вебхуков и фоновых задач в `worker`; +- ошибки проксирования `/api`, `/auth` и theme assets во `frontend`; +- ошибки авторизации Mini App и Telegram OAuth. + +## Frontend proxy, `/api`, `/auth` и theme assets + +`frontend` - это nginx-контейнер Mini App. Он отдает статику и проксирует Web App маршруты во внутренний backend WebApp server на `backend:8081`. + +Сначала смотрите nginx-логи: + +```bash +docker compose logs -f frontend +``` + +Если видите `404`, `502`, `upstream` или `connect() failed`, проверьте маршруты: + +- `/api/*` и `/auth/*` должны попадать в `frontend:80`, а уже frontend проксирует их в `backend:8081`; +- `/webapp-logo`, `/webapp-uploaded-logo/*`, `/webapp-favicon/*`, `/webapp-theme-css/*` и `/webapp-theme-assets/*` тоже проксируются через frontend; +- внешний reverse proxy не должен отдельно уводить `/api` или `/auth` на webhook-сервер `backend:8080`. + +Быстрые проверки снаружи: + +```bash +curl -i https://app.domain.com/health +curl -i https://app.domain.com/api/bootstrap +curl -i https://app.domain.com/auth/telegram/start +curl -i https://app.domain.com/webapp-theme-css/dark/style.css +``` + +Если `/health` отвечает, а `/api/bootstrap` или theme assets падают, смотрите одновременно frontend и backend: + +```bash +docker compose logs -f frontend backend +``` + +Где проверять конфигурацию: + +- frontend nginx: `deploy/docker/frontend/nginx.conf`; +- внешний Caddy/Nginx: `deploy/examples/caddy/Caddyfile` или `deploy/examples/nginx/nginx.conf.template`; +- Web App домен: `SUBSCRIPTION_MINI_APP_URL`, он должен быть публичным HTTPS URL frontend, без `/api`, `/auth` или webhook-пути; +- WebApp server backend: `WEBAPP_ENABLED=True`, `WEBAPP_SERVER_HOST=0.0.0.0`, `WEBAPP_SERVER_PORT=8081`. + +## Mini App auth и Telegram OAuth + +Ошибки авторизации почти всегда видны в `backend`, потому что проверка Telegram Mini Apps `initData`, Telegram OAuth `id_token`, nonce/state и сессий выполняется на backend WebApp server. + +```bash +docker compose logs -f backend +``` + +Ищите сообщения: + +- `Telegram WebApp initData hash mismatch`; +- `Telegram WebApp initData auth_date is stale`; +- `Failed to validate Telegram WebApp initData`; +- `Telegram OAuth nonce mismatch`; +- `Telegram OAuth ID token is stale`; +- `Failed to validate Telegram OAuth ID token`; +- `Telegram OAuth token exchange failed`; +- `Telegram OAuth callback failed`; +- `WebApp auth failed`. + +Для Mini App внутри Telegram проверьте: + +- `SUBSCRIPTION_MINI_APP_URL` совпадает с доменом, указанным в BotFather Mini Apps; +- открывается именно HTTPS frontend-домен, а не backend webhook-домен; +- время на сервере синхронизировано, иначе `auth_date is stale`; +- `WEBAPP_AUTH_MAX_AGE_SECONDS` не слишком маленький; +- `WEBAPP_SESSION_SECRET` постоянный между рестартами. + +Для Telegram OAuth вне Mini App проверьте: + +- `TELEGRAM_OAUTH_CLIENT_ID` и `TELEGRAM_OAUTH_CLIENT_SECRET`; +- callback в Telegram OAuth/BotFather: `https://app.domain.com/auth/telegram/callback`; +- `/auth/telegram/start` и `/auth/telegram/callback` проходят через frontend nginx в `backend:8081`; +- в браузере после callback нет статуса `telegram_auth=invalid_state`, `invalid_token`, `not_configured`, `unauthorized` или `failed`. + +Подробности по маршрутам и настройке OAuth: [Web App / Mini App](../features/web-app.md). + +## После изменения конфигурации + +```bash +docker compose up -d +docker compose logs -f backend worker frontend +``` + +См. также [проблемы](issues.md) и [развертывание](../deployment.md). diff --git a/tests/test_migration_doc_accuracy.py b/tests/test_migration_doc_accuracy.py index dafbfca..5af59d7 100644 --- a/tests/test_migration_doc_accuracy.py +++ b/tests/test_migration_doc_accuracy.py @@ -1,4 +1,4 @@ -"""Pin facts that ``docs/migration-to-minishop.md`` and +"""Pin facts that ``docs/migrations/remnawave-tg-shop.md`` and ``scripts/migrate_to_minishop.sh`` rely on. Both documents are written for a user upgrading from ``remnawave-tg-shop`` @@ -18,7 +18,7 @@ import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] -DOC_PATH = REPO_ROOT / "docs" / "migration-to-minishop.md" +DOC_PATH = REPO_ROOT / "docs" / "migrations" / "remnawave-tg-shop.md" SCRIPT_PATH = REPO_ROOT / "scripts" / "migrate_to_minishop.sh" COMPOSE_FILES = ( REPO_ROOT / "docker-compose.yml", @@ -64,14 +64,14 @@ class MigrationDocumentationFactsTests(unittest.TestCase): missing = sorted(name for name in EXPECTED_CONTAINER_NAMES if name not in self.doc) self.assertFalse( missing, - f"migration-to-minishop.md is missing container names from current compose: {missing}", + f"migrations/remnawave-tg-shop.md is missing container names from current compose: {missing}", ) def test_doc_lists_every_volume_in_current_compose(self): missing = sorted(name for name in EXPECTED_VOLUME_NAMES if name not in self.doc) self.assertFalse( missing, - f"migration-to-minishop.md is missing volume names from current compose: {missing}", + f"migrations/remnawave-tg-shop.md is missing volume names from current compose: {missing}", ) def test_doc_warns_about_renamed_telegram_webhook_secret(self): @@ -234,7 +234,7 @@ class DocComposeFileReferencesTests(unittest.TestCase): self.assertIn(relpath, doc) self.assertTrue( (REPO_ROOT / relpath).is_file(), - f"{relpath} is referenced in migration-to-minishop.md but missing on disk", + f"{relpath} is referenced in migrations/remnawave-tg-shop.md but missing on disk", ) def test_doc_references_migrator_module_path(self):