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>
This commit is contained in:
Savsis? 2026-09-13 23:27:06 +05:00
parent 1747d63539
commit 7d140711fd
8 changed files with 372 additions and 78 deletions

View file

@ -182,3 +182,61 @@ jobs:
print("all v1.1.0 feature smoke tests passed")
PYEOF
- name: Smoke test live settings (.env-backed plans/toggles/HWID/credentials, no restart)
env:
BOT_TOKEN: "123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
BOT_USERNAME: "x"
ADMIN_IDS: "1"
ADMIN_PANEL_PASSWORD: "ci-test-password-not-real"
PANEL_DOMAIN: "panel.test"
SUB_DOMAIN: "sub.test"
SITE_DOMAIN: "test"
XRAY_PUBLIC_KEY: "x"
XRAY_SHORT_ID_TCP: "x"
XRAY_SHORT_ID_GRPC: "x"
XRAY_SHORT_ID_XHTTP: "x"
run: |
python - << 'PYEOF'
import hashlib
import hmac
with open(".env", "a", encoding="utf-8") as f:
f.write("PLATEGA_SECRET=old_secret\n")
f.write("PLATEGA_ENABLED=true\n")
f.write("PLATEGA_MERCHANT_ID=m1\n")
import legal
import settings
import payments
plans = settings.get_plans_by_code()
assert plans["1m"]["price"] > 0, "default price should come from config before any .env override"
settings.set_plan_prices({"1m": 4242})
assert settings.get_plans_by_code()["1m"]["price"] == 4242, "price edit should apply live, no reimport"
assert settings.get_plans_by_code()["7d"]["price"] != 4242, "unrelated plan must stay untouched"
assert settings.get_hwid_settings()["enabled"] is False
legal.update_env_var("HWID_LIMIT_ENABLED", "true")
legal.update_env_var("HWID_FALLBACK_LIMIT", "9")
hwid = settings.get_hwid_settings()
assert hwid["enabled"] is True and hwid["fallback_limit"] == 9, "HWID settings should apply live"
body = b'{"transactionId":"t1","status":"CONFIRMED"}'
sig_old = hmac.new(b"old_secret", body, hashlib.sha256).hexdigest()
assert payments.verify_platega_signature(body, sig_old), "signature must verify against the current secret"
legal.update_env_var("PLATEGA_SECRET", "rotated_secret")
assert not payments.verify_platega_signature(body, sig_old), "OLD signature must be rejected right after rotation, same process, no restart"
sig_new = hmac.new(b"rotated_secret", body, hashlib.sha256).hexdigest()
assert payments.verify_platega_signature(body, sig_new), "NEW signature must verify immediately after rotation, same process, no restart"
for _ in range(5):
legal.update_env_var("HWID_FALLBACK_LIMIT", "9")
with open(".env", encoding="utf-8") as f:
lines = [l for l in f.readlines() if l.startswith("HWID_FALLBACK_LIMIT=")]
assert len(lines) == 1, "repeated writes to the same key must not duplicate .env lines"
print("live settings: prices/HWID/credential-rotation all apply with zero reimport OK")
PYEOF