mbs-panel/.env.example

79 lines
3.2 KiB
Text
Raw Normal View History

# Copy this to .env and fill in real values. Never commit .env.
feat: custom brand name everywhere + a working client-facing site out of the box User ask, paraphrased: install it, get help wiring up payments, and immediately have a ready site under your own name — not "MBS Panel" plastered everywhere and a bunch of manual follow-up. Two things were actually broken/missing, found by tracing every surface a real customer or the operator would see: 1. "MBS Panel" was hardcoded in ~20 places (bot messages, subscription page, admin panel splash/title/sidebar, legal pages, 2FA issuer, install.sh) with zero way to change it short of editing source. New BRAND_NAME config value (config.py default "MBS Panel", so this is 100% backward compatible for existing installs) wired through everywhere via the same live-settings pattern from the last commit (settings.get_brand_name(), no restart needed anywhere it's used). New Настройки → «Название» section in the admin panel to change it. 2. site/index.html and site/cabinet.html — a fully-built landing page + personal-cabinet template, already in the repo — were never actually served by anything. Not mounted by FastAPI, not deployed by install.sh, not linked from anywhere. Pure dead weight: a repo that looked like it shipped a client site but didn't. Now legal.py gets a render_site_page() (same {{TOKEN}} substitution + HTML-escaping as the existing offer/privacy renderer, new tokens: BRAND_NAME, SITE_DOMAIN, SUB_DOMAIN, BOT_USERNAME) and GET "/" serves the branded landing page on any host that isn't PANEL_DOMAIN (in practice: SUB_DOMAIN, which nginx already routes to this backend — zero install.sh/nginx/certbot changes needed, so this is live on every existing install without an upgrade step beyond `mbs update`). GET /cabinet.html serves the cabinet. Landing page's pricing section now fetches real, live prices from a new public GET /api/plans instead of showing static duration labels with no numbers. Also fixed along the way, same staleness-bug class as the payments/HWID fix last commit, found by grepping for every remaining frozen `from config import ...` in api.py: BOT_TOKEN/BOT_USERNAME were still frozen constants in api.py (mbs-api never restarts itself). Concretely this meant: changing the bot via Настройки → Telegram-бот would leave _tg_send_message (payment-received notifications) silently trying the OLD token, admin_get_bot_settings showing the OLD username right after a successful save, and gift-code links pointing at the OLD bot — all until a manual mbs restart, same shape as the Platega-secret bug fixed last commit. Added settings.bot_credentials(), wired it through every call site (hoisted out of loops where relevant, same N+1 discipline as always), removed the now-stale "выполни mbs restart" copy from the bot settings hint. legal.py's own BOT_USERNAME import was frozen too (used by the /offer and /privacy {{BOT_USERNAME}} token) — switched to reading it live in-module (no settings.py import from legal.py, would've been circular since settings.py already imports legal.py for the env reader). install.sh: new interactive prompt for the brand name (default "MBS Panel", so hitting enter reproduces today's behavior exactly), written to .env, echoed in the final summary along with the now-live site URL. Verification: same story as always — api.py/bot.py still can't import locally (no pydantic-core wheel for Python 3.14 on this machine). py_compile + pyflakes clean across the whole repo. Real runtime test against an isolated .env fixture: brand name and bot-credential live reads (no reimport), render_site_page() token substitution correctness on the actual site/index.html and site/cabinet.html files including an XSS check (brand name containing <script> comes out HTML-escaped), and a regression check that adding the BRAND_NAME token to the existing legal.render() didn't break offer.html/privacy.html. Extracted SUB_PAGE_TEMPLATE/SUB_PAGE_EXPIRED_TEMPLATE via ast from api.py (can't import the module, but can pull the string constants) and ran the real .format() calls against them to catch any brace-escaping mistake in the new {brand_name} placeholder — CSS braces in those templates are already double-escaped for .format(), easy to get wrong. Extracted and node --check'd admin.html's whole inline script, div-tag-balance check on the full file. install.sh's new prompt+heredoc snippet run standalone with piped stdin (both a brand name with spaces and an empty/default input), round-tripped the resulting .env back through the real env-parsing logic. Extended the existing CI "app wiring" step (which does import api/bot for real on Linux) with branding assertions calling the actual route functions directly (api.root(), api.public_plans(), api.public_branding()) — ran every part of that step's new logic that doesn't need api.py locally first, to catch what's catchable before trusting the rest to CI once the account's abuse-review lifts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 23:52:54 +05:00
# 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
# Telegram user IDs allowed into the bot's admin menu and the web panel, comma-separated
ADMIN_IDS=111111111,222222222
# Web panel login password — must be a real random string, min 8 chars
# (panel refuses to start on "change-me"/"admin"/etc). Generate one: openssl rand -base64 12
# Change it any time with: mbs pass
ADMIN_PANEL_PASSWORD=
# Domains — point all three A records at this server's IP (see README)
PANEL_DOMAIN=panel.example.com
SUB_DOMAIN=sub.example.com
SITE_DOMAIN=example.com
feat: optional custom admin login path — matches a Remnawave-listed security measure Marzban doesn't have Pulled a fresh copy of docs.rw's own Remnawave-vs-Marzban comparison table (not working from memory of an earlier read) to check what's still genuinely different after tonight's run of fixes — most rows already match or beat both panels (multi-admin, 2FA, HWID limits, backup/restore, host sorting, config validation, node autonomy, on-hold status as of a few commits ago). One concrete, bounded, unclaimed row: "Security measures in documentation" lists CF zero trust / custom path / Telegram OAuth / 2FA for Remnawave, nothing for Marzban. We already had 2FA and rate-limiting; custom path was the missing, actually implementable piece — everything else in that row is deployment guidance, not panel code. New ADMIN_PATH env var (config.py, defaults to "admin" — every existing install keeps working exactly as before with zero action needed). The page-serving route moves to whatever path is configured; root() on PANEL_DOMAIN only falls through to serving admin.html when ADMIN_PATH is still the default, otherwise it shows the same branded landing page every other domain gets — so a scanner or a human guessing "/admin" finds nothing once this is set, not even a redirect that confirms something lives there. Deliberately scoped to ONLY the page route. /admin/api/* stays fixed — it's already behind real cookie+session auth (verified this while auditing: every mutating admin route either calls require_admin() or the equivalent _require_current_admin(), checked programmatically via ast rather than trusting my memory of having added the check everywhere — found nothing actually missing, which is itself worth knowing, not just assumed). Moving the API namespace too would be a much bigger, riskier rewrite of every @app decorator in the file for no real security gain over what auth already provides. Deliberately NOT exposed in the Settings UI, unlike almost everything else made live-editable tonight. This one genuinely needs a process restart to take effect (FastAPI resolves routes at import time, not per-request), and a typo saved through the UI followed by a restart is a real self-lockout risk with no web-based way back — same tier as PANEL_DOMAIN/SUB_DOMAIN, which are also .env-only for the same reason. .env + SSH is the correct blast radius for a setting that can lock you out. Verification: config.py's normalization (strip slashes, empty/lone- slash/repeated-slash input all falling back to "admin" rather than accidentally producing a route at bare "/") tested directly — 8 cases. AST-extracted the updated root() out of api.py (still can't import the module locally) and exercised its actual branching with a mocked FileResponse/legal/request — confirmed the default case is byte-for- byte the old behavior and the custom-path case stops serving admin.html on PANEL_DOMAIN's root. Added a dedicated CI step that does what only a real FastAPI import can prove: with ADMIN_PATH set, /xyz123secret is a registered route, plain /admin is NOT (not just supplemented — actually gone), and /admin/api/login is untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 01:58:45 +05:00
# Optional: move the admin login off the well-known /admin path (e.g. to a
# random string) so it doesn't show up to anyone scanning for /admin,
# /login etc. Leave unset for the default. This is on top of the existing
# rate-limiting and 2FA, not instead of them. Requires `mbs restart` to
# take effect (it's a route, not a setting the running process can pick up
# live) — write it down somewhere before you restart, there's no UI for
# this on purpose, only .env + SSH can get you back in if you forget it.
ADMIN_PATH=admin
# Reality identity for the local node (this same box). Generate with:
# /usr/local/bin/xray x25519
# XRAY_PUBLIC_KEY is the "Password (PublicKey)" line; keep the matching
# private key only inside /usr/local/etc/xray/config.json (never here).
XRAY_PUBLIC_KEY=
REALITY_SNI=www.wildberries.ru
# Any 16-hex-char string per transport, e.g.: openssl rand -hex 8
XRAY_SHORT_ID_TCP=
XRAY_SHORT_ID_GRPC=
XRAY_SHORT_ID_XHTTP=
# Public hostname clients connect to for the local node (A record -> this server)
DE1_ADDRESS=de1.example.com
# Payments — off by default, bot keeps handing out free subscriptions on button press.
# Flip to true only once at least one provider below is configured and its webhook is live.
feat: plan prices, payment toggles and HWID limit now editable live from the admin panel, no restart Closes the last "still .env-only" gap from the backlog (tariffs/HWID) and fixes a real bug found while building it: payment provider credentials and enabled-flags were frozen in api.py's process at import time, so a Platega secret rotation via the settings UI would leave api.py verifying inbound webhooks against the OLD secret until a manual `mbs restart` — while bot.py (which does get restarted on save) already had the new one. Same class of staleness affected HWID_LIMIT_ENABLED/HWID_FALLBACK_LIMIT and plan prices, neither of which had any settings UI at all before this. New `settings.py` module: get_plans()/get_plans_by_code() (live prices, falls back to config.py defaults), get_payment_settings(), get_hwid_settings(), yookassa_credentials()/platega_credentials(), set_plan_prices() — all backed by a new batched legal.read_env_vars() (one file read for N keys instead of N reads) and legal.update_env_var() (moved out of api.py's private _update_env_var, which is now a one-line delegate to avoid duplicating the same env-file-rewrite logic in two places). api.py and bot.py no longer import PLANS/PLANS_BY_CODE/PAYMENTS_ENABLED/ HWID_LIMIT_ENABLED/HWID_FALLBACK_LIMIT from config as frozen constants — every read goes through settings.py instead. payments.py no longer imports YOOKASSA_*/PLATEGA_* from config either; every provider call (create/check payment, verify webhook signature) reads live credentials at call time. Every call site inside a loop hoists the live lookup before the loop first (same N+1 discipline as the rest of tonight), so this doesn't regress get_subscription's hot path — one settings.get_hwid_settings() call per request, same as before. New routes: GET/POST /admin/api/payments/plan-settings (per-plan prices + a payments_enabled master toggle — there was previously no way to turn payment collection back off without deleting provider credentials), GET/POST /admin/api/hwid-settings. Both validate input strictly (prices: non-negative int; HWID limit: 1-1000) and reject the whole request instead of partially applying on bad input. admin.html: new "Тарифы" section in Платежи (price inputs rendered from the live plan list + payments toggle) and "Лимит устройств (HWID)" in Настройки, both using the existing .check checkbox / plain-input styling (no native <select>, per the earlier white-popup complaint). Removed the now-incorrect "выполни mbs restart" copy from the YooKassa/Platega settings hints and save-result messages, and added a doc-block for HWID (never had one) plus extended the Платежи doc-block to mention live-apply. Also dropped a dead `import links` in bot.py caught by pyflakes while verifying this. Verification: api.py/bot.py still can't be imported on this Windows machine (no prebuilt pydantic-core wheel for Python 3.14, confirmed again by a fresh pip attempt — same wall as every prior session), so relied on what's actually exercisable: py_compile + pyflakes (zero undefined names) across every module including api.py/bot.py, a real runtime test against an isolated .env fixture covering live price/toggle/HWID reads with zero reimport, write-idempotency (no duplicate .env lines on repeated saves), and the concrete bug this fixes end to end — computed an HMAC signature against an old Platega secret, rotated the secret via update_env_var (the same call the settings route makes), confirmed the old signature is now rejected and a new one computed against the rotated secret verifies, all in the same process with no reimport. Also ran the new CI step's exact heredoc locally byte-for-byte before adding it to ci.yml. GitHub Actions still won't trigger for this account (still under abuse-review, ticket open >2 days) so this is the same substitute-for-CI rigor used all night. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 23:27:06 +05:00
# All of this (toggle, prices, provider keys) is also editable live from the admin panel
# (Платежи tab) after first boot — no need to hand-edit this file or restart afterwards.
PAYMENTS_ENABLED=false
# Prices in RUB per plan (whole numbers). Only used when PAYMENTS_ENABLED=true.
PRICE_7D=150
PRICE_1M=399
PRICE_3M=999
PRICE_6M=1799
PRICE_1Y=2999
# ЮKassa — https://yookassa.ru, shop id + secret key from your account settings.
# Webhook to add in their dashboard: https://<PANEL_DOMAIN>/payments/webhook/yookassa
YOOKASSA_ENABLED=false
YOOKASSA_SHOP_ID=
YOOKASSA_SECRET_KEY=
# Platega — https://platega.io, merchant id + secret from your manager.
# Webhook to add in their dashboard: https://<PANEL_DOMAIN>/payments/webhook/platega
PLATEGA_ENABLED=false
PLATEGA_MERCHANT_ID=
PLATEGA_SECRET=
# Device limit (HWID) — off by default. Requires the VPN client app to send an
# x-hwid header on subscription fetch (Happ/v2rayTun-class apps do this); clients
# that don't send it get refused once enabled, so only flip this on if your users'
feat: plan prices, payment toggles and HWID limit now editable live from the admin panel, no restart Closes the last "still .env-only" gap from the backlog (tariffs/HWID) and fixes a real bug found while building it: payment provider credentials and enabled-flags were frozen in api.py's process at import time, so a Platega secret rotation via the settings UI would leave api.py verifying inbound webhooks against the OLD secret until a manual `mbs restart` — while bot.py (which does get restarted on save) already had the new one. Same class of staleness affected HWID_LIMIT_ENABLED/HWID_FALLBACK_LIMIT and plan prices, neither of which had any settings UI at all before this. New `settings.py` module: get_plans()/get_plans_by_code() (live prices, falls back to config.py defaults), get_payment_settings(), get_hwid_settings(), yookassa_credentials()/platega_credentials(), set_plan_prices() — all backed by a new batched legal.read_env_vars() (one file read for N keys instead of N reads) and legal.update_env_var() (moved out of api.py's private _update_env_var, which is now a one-line delegate to avoid duplicating the same env-file-rewrite logic in two places). api.py and bot.py no longer import PLANS/PLANS_BY_CODE/PAYMENTS_ENABLED/ HWID_LIMIT_ENABLED/HWID_FALLBACK_LIMIT from config as frozen constants — every read goes through settings.py instead. payments.py no longer imports YOOKASSA_*/PLATEGA_* from config either; every provider call (create/check payment, verify webhook signature) reads live credentials at call time. Every call site inside a loop hoists the live lookup before the loop first (same N+1 discipline as the rest of tonight), so this doesn't regress get_subscription's hot path — one settings.get_hwid_settings() call per request, same as before. New routes: GET/POST /admin/api/payments/plan-settings (per-plan prices + a payments_enabled master toggle — there was previously no way to turn payment collection back off without deleting provider credentials), GET/POST /admin/api/hwid-settings. Both validate input strictly (prices: non-negative int; HWID limit: 1-1000) and reject the whole request instead of partially applying on bad input. admin.html: new "Тарифы" section in Платежи (price inputs rendered from the live plan list + payments toggle) and "Лимит устройств (HWID)" in Настройки, both using the existing .check checkbox / plain-input styling (no native <select>, per the earlier white-popup complaint). Removed the now-incorrect "выполни mbs restart" copy from the YooKassa/Platega settings hints and save-result messages, and added a doc-block for HWID (never had one) plus extended the Платежи doc-block to mention live-apply. Also dropped a dead `import links` in bot.py caught by pyflakes while verifying this. Verification: api.py/bot.py still can't be imported on this Windows machine (no prebuilt pydantic-core wheel for Python 3.14, confirmed again by a fresh pip attempt — same wall as every prior session), so relied on what's actually exercisable: py_compile + pyflakes (zero undefined names) across every module including api.py/bot.py, a real runtime test against an isolated .env fixture covering live price/toggle/HWID reads with zero reimport, write-idempotency (no duplicate .env lines on repeated saves), and the concrete bug this fixes end to end — computed an HMAC signature against an old Platega secret, rotated the secret via update_env_var (the same call the settings route makes), confirmed the old signature is now rejected and a new one computed against the rotated secret verifies, all in the same process with no reimport. Also ran the new CI step's exact heredoc locally byte-for-byte before adding it to ci.yml. GitHub Actions still won't trigger for this account (still under abuse-review, ticket open >2 days) so this is the same substitute-for-CI rigor used all night. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 23:27:06 +05:00
# apps actually support it. Also editable live from Настройки in the admin panel.
HWID_LIMIT_ENABLED=false
HWID_FALLBACK_LIMIT=3