diff --git a/README.md b/README.md index 290c333..6b57603 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Remnawave Minishop - Telegram-бот и Web App (Mini App) для продажи - регистрация с выбором русского или английского языка; - просмотр статуса подписки, даты окончания, ссылки подключения и трафика; -- покупка подписок, пакетов трафика, докупка трафика и устройств по настроенному каталогу тарифов; +- покупка подписок, пакетов трафика, обычная и premium-докупка трафика, докупка устройств по настроенному каталогу тарифов; - Web App / Mini App с входом через Telegram или email; - пробный период, промокоды и реферальная программа; - оплата через YooKassa, FreeKassa, Platega, SeverPay, CryptoPay и Telegram Stars; @@ -20,13 +20,15 @@ Remnawave Minishop - Telegram-бот и Web App (Mini App) для продажи - админ-панель для пользователей из `ADMIN_IDS`; - статистика пользователей, подписок, платежей и синхронизации с Remnawave; -- блокировка пользователей, рассылки, промокоды и логи действий; +- блокировка пользователей, рассылки, промокоды, логи действий и настройка разрешенных параметров приложения поверх `.env`; +- редактор JSON-каталога тарифов с period/traffic-моделями, Internal Squads, premium-сквадами и HWID-пакетами; - ручная синхронизация пользователей и подписок с панелью. ## Документация - [Настройка окружения](docs/configuration.md) - основные переменные `.env`, платежи, Remnawave, пробный период и секреты. -- [Тарифы](docs/tariffs.md) - каталог тарифов, period- и traffic-модели, докупки, смена тарифа, HWID-лимиты и обработка трафика. +- [Тарифы](docs/tariffs.md) - каталог тарифов, period- и traffic-модели, обычные и premium-докупки, premium-сквады, смена тарифа, HWID-лимиты и обработка трафика. +- [Админ-панель](docs/admin.md) - права доступа, настройки, редактор тарифов, premium-сквады и сохранение JSON-каталога. - [Web App / Mini App](docs/webapp.md) - отдельный порт, домен, Telegram OAuth, email-вход и реферальные ссылки. - [Развертывание](docs/deployment.md) - Docker Compose, reverse proxy, Nginx, Caddy, вебхуки и запуск из образа. - [Миграция с remnawave-tg-shop](docs/migration-to-minishop.md) - перенос данных из прежнего стека. diff --git a/bot/app/web/frontend/src/App.svelte b/bot/app/web/frontend/src/App.svelte index 385bdfb..93f406b 100644 --- a/bot/app/web/frontend/src/App.svelte +++ b/bot/app/web/frontend/src/App.svelte @@ -256,6 +256,7 @@ let deviceTopupModalOpen = query.get("device_topup") === "1"; let changeModalOpen = query.get("change") === "1"; let topupOptions = null; + let topupOptionsRequestId = 0; let deviceTopupOptions = null; let changeOptions = null; let selectedTopupPlan = null; @@ -1817,20 +1818,23 @@ } async function loadTopupOptions(kind = topupKind) { - if (topupOptions?.topup_kind === kind || tariffActionBusy) return; + if (topupOptions?.topup_kind === kind) return; + const requestId = ++topupOptionsRequestId; tariffActionBusy = true; try { topupOptions = null; selectedTopupPlan = null; const response = await api(`/tariffs/topup-options?kind=${encodeURIComponent(kind)}`); + if (requestId !== topupOptionsRequestId || kind !== topupKind) return; if (!response?.ok) throw response; topupOptions = response; selectedTopupPlan = response.plans?.[0] || null; } catch (error) { + if (requestId !== topupOptionsRequestId || kind !== topupKind) return; showToast(error?.message || t("wa_tariff_options_failed")); topupModalOpen = false; } finally { - tariffActionBusy = false; + if (requestId === topupOptionsRequestId) tariffActionBusy = false; } } @@ -2272,6 +2276,10 @@ function openTopupModal(kind = "regular") { if (kind === "premium" ? !canOpenPremiumTopupModal : !canOpenRegularTopupModal) return; + if (topupKind !== kind) { + topupOptions = null; + selectedTopupPlan = null; + } topupKind = kind; topupModalOpen = true; loadTopupOptions(kind); @@ -2617,13 +2625,19 @@ function topupModalDescription() { if (!topupOptions) return ""; - if (topupKind === "premium") return topupOptions?.tariff_name ? t("wa_topup_for_tariff", { tariff: topupOptions.tariff_name }) : ""; + if (isPremiumTopupContext()) return topupOptions?.tariff_name ? t("wa_topup_for_tariff", { tariff: topupOptions.tariff_name }) : ""; if (singleTariffMode) return ""; return topupOptions?.tariff_name ? t("wa_topup_for_tariff", { tariff: topupOptions.tariff_name }) : ""; } + function isPremiumTopupContext() { + if (selectedTopupPlan?.sale_mode === "premium_topup") return true; + if (topupOptions?.topup_kind) return topupOptions.topup_kind === "premium"; + return topupKind === "premium"; + } + function topupModalTitle() { - if (topupKind === "premium") return premiumTitle(subscription); + if (isPremiumTopupContext()) return premiumTitle(topupOptions || subscription); return t("wa_topup_traffic"); } diff --git a/config/tariffs.example.json b/config/tariffs.example.json index 6029e9a..7c8cce1 100644 --- a/config/tariffs.example.json +++ b/config/tariffs.example.json @@ -5,6 +5,7 @@ "key": "standard", "names": { "ru": "Стандарт", "en": "Standard" }, "descriptions": { "ru": "Базовый набор серверов", "en": "Base server pool" }, + "premium_names": { "ru": "Premium-серверы", "en": "Premium servers" }, "squad_uuids": ["uuid-1", "uuid-2"], "premium_squad_uuids": ["premium-squad-uuid"], "premium_monthly_gb": 50, diff --git a/docs/admin.md b/docs/admin.md new file mode 100644 index 0000000..6379331 --- /dev/null +++ b/docs/admin.md @@ -0,0 +1,53 @@ +# Админ-панель Web App + +Админ-панель доступна пользователям, чей Telegram ID указан в `ADMIN_IDS`. В Web App такие пользователи видят раздел администрирования; все API `/api/admin/*` дополнительно проверяют сессию и `ADMIN_IDS` на сервере. + +## Возможности + +- дашборд со статистикой пользователей, платежей и синхронизации с Remnawave; +- список пользователей, карточка активной подписки, обычный и premium-трафик, платежи и действия; +- блокировка пользователей, рассылки, промокоды и просмотр логов; +- ручная синхронизация с Remnawave; +- редактор разрешенных настроек приложения из manifest-файла; +- редактор JSON-каталога тарифов; +- загрузка Internal Squads из Remnawave для выбора в тарифах. + +## Настройки + +Раздел **Система -> Настройки** позволяет менять приложение без редактирования `.env`, но только в пределах allowlist из `bot/app/web/admin_settings_manifest.py`. Даже администратор не может менять через UI произвольные атрибуты `Settings`. + +Изменения сохраняются как overrides в базе данных и применяются поверх `.env` без перезапуска. Если значение сброшено, снова используется значение из `.env`. После сохранения публичный кеш настроек Web App сбрасывается, поэтому пользователи видят изменения при следующих запросах. + +В manifest сейчас входят: + +- общие параметры: язык, валюта, ссылки поддержки, документы, обязательный канал и поведение `/start`; +- внешний вид и доступность Web App: название, цвет, логотип, emoji-логотип и `WEBAPP_ENABLED`; +- legacy-цены без JSON-каталога: периоды подписки, RUB/Stars цены и пакеты трафика; +- платежные провайдеры: включение методов, порядок кнопок, публичные параметры и секреты YooKassa, FreeKassa, Platega, SeverPay, CryptoPay и Stars; +- пробный период, реферальные бонусы, уведомления, логирование, раздел устройств, лимит устройств и legacy-лимиты трафика. + +Секретные поля помечены как secret и не должны использоваться для произвольного просмотра старых значений. Настройки, которых нет в manifest, остаются только в `.env` или коде. + +## Тарифы + +Раздел **Система -> Тарифы** работает с файлом `TARIFFS_CONFIG_PATH` (по умолчанию `config/tariffs.json`). При сохранении backend валидирует payload через `TariffsConfig`, пишет JSON в UTF-8 и сбрасывает кеш публичных данных Web App. + +Если файла еще нет, админка открывает пустой каталог. Перед сохранением должен быть хотя бы один включенный тариф, а `default_tariff` должен ссылаться на включенный тариф. + +Редактор тарифа разделен на вкладки: + +- **Основное**: ключ, модель `period`/`traffic`, видимость, названия и описания RU/EN, базовые Internal Squads, HWID-лимит, месячный лимит или курс конвертации; +- **Цены**: периоды и цены для `period`, пакеты GB и цены для `traffic`; +- **Докупки**: обычные пакеты докупки трафика для `period`; для `traffic` отдельные докупки не нужны, пользователь повторно покупает пакеты из `traffic_packages`; +- **Premium**: названия premium-раздела RU/EN, premium Internal Squads, месячный premium-лимит и RUB/Stars пакеты premium-докупки; +- **Устройства**: RUB/Stars пакеты докупки HWID-устройств. + +Базовые и premium Internal Squads выбираются из Remnawave через `/api/admin/panel/internal-squads`. Если панель недоступна, можно сохранить уже существующие UUID в JSON, но выпадающий список не загрузится. + +## Практические замечания + +Не меняйте `key` опубликованного тарифа без миграции активных подписок: подписки, платежи и смена тарифа ссылаются на этот ключ. + +Отключенный тариф исчезает с витрины, но активные подписки с его `tariff_key` продолжают существовать. Удаление тарифа безопасно только если не осталось активных подписок, которым нужна докупка, продление или смена с этого тарифа. + +Для Docker важно, чтобы путь `TARIFFS_CONFIG_PATH` был доступен на запись контейнеру. В dev-compose каталог `./config` монтируется в `/app/config`, поэтому админка может сохранять `config/tariffs.json`. diff --git a/docs/configuration.md b/docs/configuration.md index 7dfe324..a2a212d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -87,6 +87,8 @@ docker compose -f docker-compose-dev.yml up -d --build --force-recreate Переопределения из веб-админки сохраняются в БД и применяются поверх `.env` без перезапуска. Для платежных методов кнопка отображается только если соответствующий `*_ENABLED=true` и сервис настроен. +Редактор тарифов в админке сохраняет не override в БД, а сам JSON-файл `TARIFFS_CONFIG_PATH`. Редактор настроек админки, наоборот, работает через allowlist из `bot/app/web/admin_settings_manifest.py` и сохраняет overrides в БД. Через него можно менять только заявленные в manifest параметры приложения; остальные параметры остаются в `.env`. Подробнее: [admin.md](admin.md). + ## Web App и email-вход | Переменная | Назначение | diff --git a/docs/tariffs.md b/docs/tariffs.md index 87e935f..7ca05d4 100644 --- a/docs/tariffs.md +++ b/docs/tariffs.md @@ -7,6 +7,12 @@ JSON-каталог может содержать несколько тарифов разных моделей: подписки на срок, пакеты трафика без срока действия, разные наборы Internal Squads, лимиты устройств и пакеты докупки. Пример формата: [config/tariffs.example.json](../config/tariffs.example.json). +Коротко по моделям: + +- `period` - подписка на срок с месячным лимитом трафика и опциональной докупкой GB поверх месячного лимита; +- `traffic` - покупка пакетов GB без пользовательского срока действия, где повторная покупка добавляет трафик к текущему остатку; +- premium-сквады - дополнительный набор Internal Squads внутри любого тарифа, с отдельным названием, счетчиком, месячным лимитом и отдельными пакетами докупки. + ## Управление через админку Каталог тарифов можно настраивать из Web App админки: раздел **Система → Тарифы**. Админка читает и сохраняет файл из `TARIFFS_CONFIG_PATH`, валидирует данные той же моделью `TariffsConfig`, что и бот, и атомарно перезаписывает JSON только после успешной проверки. @@ -18,10 +24,14 @@ JSON-каталог может содержать несколько тариф - выбор тарифа по умолчанию; - настройка `period`-тарифов: месячный лимит, периоды, RUB/Stars цены, пакеты докупки трафика; - настройка `traffic`-тарифов: пакеты GB, RUB/Stars цены, курс конвертации; -- настройка Internal Squads, базового HWID-лимита и пакетов докупки устройств. +- настройка базовых Internal Squads из списка Remnawave; +- настройка premium-раздела: названия RU/EN, premium Internal Squads, месячный premium-лимит и RUB/Stars пакеты докупки premium-трафика; +- настройка базового HWID-лимита и пакетов докупки устройств. После сохранения изменения применяются к новым запросам Web App сразу, потому что конфиг тарифов загружается из JSON при обращении. Уже созданные подписки сохраняют свой `tariff_key`; при удалении или отключении тарифа проверьте, что активные подписки с этим ключом не требуют дальнейшего продления или смены. +Подробности по админ-панели, правам доступа, сохранению настроек и списку разделов есть в [admin.md](admin.md). + ## Как выбирается режим Если файл из `TARIFFS_CONFIG_PATH` существует и проходит валидацию, используется каталог тарифов. В этом режиме `TRAFFIC_PACKAGES` и цены подписок из `.env` не формируют витрину продаж, потому что цены и пакеты берутся из JSON. @@ -46,6 +56,7 @@ JSON-каталог может содержать несколько тариф "key": "standard", "names": { "ru": "Стандарт", "en": "Standard" }, "descriptions": { "ru": "Базовый набор серверов" }, + "premium_names": { "ru": "Premium-серверы", "en": "Premium servers" }, "squad_uuids": ["uuid-1"], "billing_model": "period", "monthly_gb": 500, @@ -71,9 +82,10 @@ JSON-каталог может содержать несколько тариф | `tariffs[].descriptions` | Описания тарифа по языкам. | | `tariffs[].enabled` | Доступность тарифа на витрине. | | `tariffs[].squad_uuids` | Internal Squads Remnawave для пользователей тарифа. | +| `tariffs[].premium_names` | Название premium-раздела по языкам. Используется в карточке лимита, модалке докупки premium-трафика и предупреждениях. Если поле не задано, используется `Premium-серверы` / `Premium servers`. | | `tariffs[].premium_squad_uuids` | Internal Squads с отдельным premium-лимитом. Ноды для учета берутся автоматически из accessible nodes этих сквадов через API панели. | | `tariffs[].premium_monthly_gb` | Отдельный месячный лимит трафика по premium-сквадам. `0` или отсутствие поля отключает отдельное ограничение. | -| `tariffs[].premium_topup_packages` | Пакеты докупки premium-трафика: `{ "gb": 10, "price": 99 }`. | +| `tariffs[].premium_topup_packages` | Пакеты докупки premium-трафика в формате `{ "rub": [{ "gb": 10, "price": 99 }], "stars": [...] }`. Требуют `premium_squad_uuids`. | | `tariffs[].billing_model` | Модель тарифа: `period` или `traffic`. | | `tariffs[].hwid_device_limit` | Базовый лимит HWID-устройств. `0` означает безлимит, отсутствие поля использует `USER_HWID_DEVICE_LIMIT`. | | `tariffs[].hwid_device_packages` | Пакеты докупки устройств: `{ "count": 1, "price": 99 }`. | @@ -124,6 +136,7 @@ JSON-каталог может содержать несколько тариф ```json { "squad_uuids": ["standard-squad-uuid"], + "premium_names": { "ru": "Premium-серверы", "en": "Premium servers" }, "premium_squad_uuids": ["premium-squad-uuid"], "premium_monthly_gb": 50, "premium_topup_packages": { @@ -144,6 +157,8 @@ JSON-каталог может содержать несколько тариф - докупленный premium-трафик не сгорает в конце месяца: каждый месяц сначала расходуется `premium_monthly_gb`, а докупленный остаток уменьшается только на трафик сверх месячного лимита; - при новом календарном месяце счетчик premium-трафика и `premium_topup_used_bytes` сбрасываются, но `premium_topup_balance_bytes` переносится дальше. +Если `premium_squad_uuids` заданы, но `premium_monthly_gb` пустой или `0` и нет `premium_topup_packages`, premium-сквады работают как дополнительный доступ без отдельного ограничения. Если заданы `premium_topup_packages` или положительный `premium_monthly_gb`, `premium_squad_uuids` обязательны. + Состояние хранится в подписке: - `premium_baseline_bytes` - базовый premium-лимит тарифа; @@ -155,9 +170,11 @@ JSON-каталог может содержать несколько тариф В пользовательском Web App premium-лимит показывается отдельной карточкой: использовано, лимит, остаток, докупленный переносимый остаток и список серверов/сквадов, на которые действует отдельное ограничение. В Telegram-разделе “Моя подписка” выводится тот же блок. -Предупреждения по premium-лимиту отправляются отдельно от обычного трафика на тех же процентах `TARIFF_TRAFFIC_WARNING_LEVELS`. Сообщение объясняет, что это именно premium-серверы, перечисляет серверы/сквады и ведет пользователя в докупку premium-трафика. +Обычная докупка и premium-докупка показываются отдельно. Обычная докупка использует `topup_packages` у `period`-тарифа или `traffic_packages` у `traffic`-тарифа. Premium-докупка использует только `premium_topup_packages`, получает `sale_mode=premium_topup` и в заголовке показывает `premium_names`, а не название обычной докупки. -В Web App админке premium-сквады можно выбрать из выпадающего списка. Список берется из API Remnawave, поэтому UUID не нужно копировать вручную. +Предупреждения по premium-лимиту отправляются отдельно от обычного трафика на тех же процентах `TARIFF_TRAFFIC_WARNING_LEVELS`. Сообщение использует название из `premium_names`, перечисляет серверы/сквады и ведет пользователя в докупку premium-трафика. + +В Web App админке premium-сквады можно выбрать из выпадающего списка на вкладке **Premium** в редакторе тарифа. Список берется из API Remnawave (`/api/admin/panel/internal-squads`), поэтому UUID обычно не нужно копировать вручную. ## Traffic-тарифы diff --git a/docs/webapp.md b/docs/webapp.md index e4258f9..6a5146c 100644 --- a/docs/webapp.md +++ b/docs/webapp.md @@ -7,12 +7,15 @@ Web App запускается в том же контейнере, что и б - текущую ссылку подключения; - статус и дату окончания подписки; - использованный и доступный трафик; +- отдельную карточку premium-трафика, если у активного тарифа настроены premium-сквады и premium-лимит; - доступные тарифы, способы оплаты и платежный статус; -- смену тарифа и докупку трафика при настроенном каталоге тарифов; +- смену тарифа, обычную докупку трафика и докупку premium-трафика при настроенном каталоге тарифов; - раздел "Мои устройства" при `MY_DEVICES_SECTION_ENABLED=True`; - реферальную ссылку и статистику приглашений; - привязку email и Telegram к одному аккаунту. +Для администраторов из `ADMIN_IDS` Web App также показывает админ-панель: статистику, пользователей, рассылки, промокоды, логи, настройки и редактор тарифов. Подробности: [admin.md](admin.md). + ## Настройки `.env` ```env