diff --git a/.env.example b/.env.example index 2bb624d..1e90b35 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,9 @@ # Copy this to .env and fill in real values. Never commit .env. +# Shown to clients everywhere: site, bot, subscription page, offer/privacy, panel +# login. Also editable live from Настройки in the admin panel, no restart needed. +BRAND_NAME=MBS Panel + # From @BotFather BOT_TOKEN=123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BOT_USERNAME=YourBot_robot diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e396f5f..6d0ae9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,6 +109,36 @@ jobs: assert db.get_device(1, "hwid-aaaaaaaaaa") is not None print("app wiring + payments + HWID logic OK") + + import legal + import settings + + assert settings.get_brand_name() == "MBS Panel" + legal.update_env_var("BRAND_NAME", "CI Test Brand") + assert settings.get_brand_name() == "CI Test Brand" + + index_html = legal.render_site_page("index.html") + assert "CI Test Brand" in index_html + assert "MBS Panel" not in index_html + assert "example.com" not in index_html + assert "YourBot_robot" not in index_html + assert "{{" not in index_html and "}}" not in index_html + + cabinet_html = legal.render_site_page("cabinet.html") + assert "CI Test Brand" in cabinet_html + assert "{{" not in cabinet_html and "}}" not in cabinet_html + + fake_request = type("FakeRequest", (), {"headers": {}})() + root_resp = api.root(fake_request) + assert "CI Test Brand" in root_resp + + plans_resp = api.public_plans() + assert plans_resp["plans"][0]["code"] == "7d" + + branding_resp = api.public_branding() + assert branding_resp["brand_name"] == "CI Test Brand" + + print("branding: site templates + public routes render live, no restart OK") PYEOF - name: Smoke test TOTP, backup/restore, node reorder, multi-admin, rate-limit diff --git a/README.md b/README.md index 8b225cd..8c04012 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ ## Что внутри - **Бот** (aiogram 3) — выдача подписок по кнопкам, гифт-коды, привязка тарифов (7 дней / месяц / 3 месяца / полгода / год), автоматическое отключение по истечении подписки (не раз в полчаса, а раз в 90 секунд — важно, чтобы просрочка реально обрывала доступ, а не продолжала работать). -- **API + сайт** (FastAPI) — страница подписки со ссылкой `happ://` и QR-кодом, личный кабинет, JSON API для сайта. +- **API + сайт** (FastAPI) — страница подписки со ссылкой `happ://` и QR-кодом, готовый клиентский лендинг + личный кабинет (живут прямо в панели, подставляют название и реальные тарифы сами, ничего отдельно хостить не надо). +- **Своё название бренда** — панель, бот, сайт, страница подписки, оферта/политика показывают одно и то же настраиваемое название вместо дефолтного «MBS Panel», меняется в один клик из Настроек, применяется сразу. - **Админ-панель** (чистый HTML/CSS/JS, без фреймворков и сборки) — дашборд, полная карточка юзера (история подписок, ручная выдача, устройства), подписки, гифт-коды, ноды (полное редактирование, не только вкл/выкл), трафик по Stats API самого Xray со сбросом счётчика по клику. - **Мультинодовость** — добавляешь новую ноду в панели, получаешь одну команду `bash <(curl ...)`, вставляешь на чистый сервер — нода сама ставит Xray, генерит ключи, регистрируется в панели. Как у Remnawave/3x-ui, только свой велосипед. - **Протоколы на выбор при добавлении ноды**: VLESS TCP+Reality, VLESS gRPC+Reality, VLESS XHTTP+Reality, VLESS WS+TLS (с реальным Let's Encrypt сертификатом), Hysteria2 (QUIC, отдельный процесс, obfs). @@ -129,6 +130,7 @@ bash <(curl -Ls https://mbs.savsis.xyz/install.sh) - Зайди на `https://panel.example.com`, залогинься паролем из вывода скрипта. - Смени пароль в любой момент: `mbs pass новый_пароль` (без аргумента — сгенерит случайный). - В боте у себя (Telegram ID из ADMIN_IDS) появится админ-меню. +- На `https://sub.example.com` уже живёт готовый клиентский сайт (лендинг + личный кабинет) с подставленным названием и реальными тарифами — ничего отдельно разворачивать не нужно. Название меняется в Настройки → «Название» в панели, применяется сразу везде (сайт, бот, страница подписки, оферта/политика). В конце установки `install.sh` шлёт один пинг на `stats.api.savsis.xyz` (только название ОС) — просто счётчик "сколько раз панель установили", никаких доменов/токенов/паролей туда не уходит, IP не сохраняется. Отключить: `MBS_SKIP_STATS=1 sudo bash install.sh`. diff --git a/admin.html b/admin.html index 1e175c7..e442907 100644 --- a/admin.html +++ b/admin.html @@ -278,7 +278,7 @@
Настройки → «Название» — своё название бренда вместо дефолтного «MBS Panel», показывается сразу везде: заголовок и сайдбар панели, сообщения бота, страница подписки, оферта/политика, otpauth-issuer в приложении-аутентификаторе при включении 2FA. Применяется мгновенно, без рестарта.
+На домене подписок (SUB_DOMAIN) панель теперь сама отдаёт готовый клиентский сайт — корень (/) рендерит site/index.html (лендинг с живыми тарифами из /api/plans), /cabinet.html — личный кабинет по токену из бота. Оба шаблона лежат в репо (site/) — правишь HTML/CSS напрямую, если нужен свой дизайн, панель только подставляет название/домены/юзернейм бота при каждом запросе.
Локальная нода (обычно de1) — Xray на том же сервере, что и панель, управляется напрямую правкой config.json. Управляемые ноды — отдельные серверы, панель ходит на них по SSH management-ключу (генерится сам при первом добавлении ноды, публичная часть раздаётся install-скриптом ноды — панель никогда не просит пароль от нового сервера).
Показывается везде, где сейчас видят клиенты и ты сам: сайт, бот, страница подписки, оферта/политика, вход в панель.
+Применяется сразу везде, без рестарта.
+ +Сейчас: — (токен: —)
Панель сама проверит токен у Telegram (запрос getMe) перед применением и подставит настоящий юзернейм бота — придумывать не нужно. После смены перезапустится только бот; если уведомления от api (например об оплате) продолжат идти со старого бота, выполни на сервере mbs restart.
Панель сама проверит токен у Telegram (запрос getMe) перед применением и подставит настоящий юзернейм бота — придумывать не нужно. Применяется сразу — бот перезапускается сам, а панель (уведомления об оплате, ссылки на бота) подхватывает новый токен и юзернейм без рестарта.
Сохранено — применилось сразу
'; + } catch (e) { + result.innerHTML = 'Не получилось: ' + esc(e.message) + '
'; + } +} + async function loadBotSettings() { const data = await api("/admin/api/settings/bot"); document.getElementById("settings-bot-username").textContent = "@" + data.username; @@ -1669,8 +1705,18 @@ async function createManualNode() { loadNodes(); } +function applyBrandName(name) { + if (!name) return; + document.title = name; + const splash = document.getElementById("splash-brand-name"); + if (splash) splash.textContent = name; + const sidebar = document.getElementById("sidebar-brand-name"); + if (sidebar) sidebar.textContent = name; +} + (async function init() { initDropdowns(); + fetch("/api/branding").then((r) => r.json()).then((d) => applyBrandName(d.brand_name)).catch(() => {}); try { const me = await api("/admin/api/me"); if (me.authenticated) showApp(); else showLogin(); diff --git a/api.py b/api.py index 477f6bb..8693078 100644 --- a/api.py +++ b/api.py @@ -21,7 +21,7 @@ import settings import totp import webhooks import xray_manager -from config import SITE_DOMAIN, SUB_DOMAIN, PANEL_DOMAIN, BOT_USERNAME, BOT_TOKEN, BASE_DIR +from config import SITE_DOMAIN, SUB_DOMAIN, PANEL_DOMAIN, BASE_DIR HWID_RE = re.compile(r"^[a-zA-Z0-9=-]{10,64}$") ENV_PATH = os.path.join(BASE_DIR, ".env") @@ -74,7 +74,7 @@ SUB_PAGE_TEMPLATE = """ -