init_node.sh раньше запускал образ вообще без CLI-аргументов — все
провижинингованные ноды молча использовали дефолты и не требовали
авторизации, несмотря на то что admin-панель как будто настраивала
per-node SNI/порт. Заодно убирает захардкоженный IP control plane.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ADMIN_TG_ID нигде не читается с тех пор, как admin_guard заменили на ролевую
систему (owner/staff/node_manage/partner_guard) — оставлять его в примерах
вводит в заблуждение. PROXY_INTERNAL_SECRET, наоборот, обязателен (main.rs
падает без него при старте), но отсутствовал во всех трёх .env*.example.
Заодно поправлен src/modules/auth/README.md — описывал admin_guard,
которого больше не существует.
Добавляет 52 теста (было 7), приоритет: биллинг -> auth -> RBAC-гварды ->
staff/выводы средств -> hack-сессии -> рефералы -> proxy_internal.
Попутно найдены и исправлены реальные баги:
- migrations/0004: ALTER TABLE auth_sessions падал на чистой БД, потому что
0002 эту таблицу уже дропнул — ломало --migrate на CI/новом инстансе и
весь sqlx::test. Заменено на CREATE TABLE IF NOT EXISTS.
- request_withdrawal: гонка при параллельных заявках на вывод (не было
FOR UPDATE) — партнёр мог одновременно вывести баланс кратно больше
доступного. Новый repository::request_partner_withdrawal блокирует
строку партнёра и пересчитывает остаток в одной транзакции.
- get_referral_stats: SUM() над BIGINT возвращал NUMERIC, sqlx падал на
ColumnDecode для любого юзера с рефералами.
- reward_referrer: начислял рефереру 10% при каждой повторной покупке
того же приведённого юзера вместо разового бонуса — добавлен фильтр
WHERE status = 'pending'.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ролевая система вместо привязки к Telegram
----------------------------------------
Раньше единственный способ получить админ-доступ — admin_guard, пускавший
по ADMIN_TG_ID или role=="admin" (без сеттера для роли). Заменяет на
Owner/Moderator/Partner:
- migrations/0005_roles_and_partners.sql — username/password_hash на users
(NULL у обычных Ghost/Telegram-юзеров, тот же паттерн, что и у seed_hash/
tg_id), can_manage_nodes, commission_percent, partner_code,
referred_by_partner_id; новые таблицы partner_commissions и
withdrawal_requests.
- auth/guard.rs — admin_guard заменён на owner_guard/staff_guard/
node_manage_guard/partner_guard; ADMIN_TG_ID убран из ApiState и main.rs.
- auth/service.rs — hash_password/login_staff со случайной Argon2-солью на
каждого юзера (не общий фиксированный ARGON_SALT, который годится для
высокоэнтропийных seed-логинов, но не для человеческих паролей).
- POST /api/v1/auth/admin/login — вход модератора/партнёра/овнера паролем,
переиспользует существующий issue_session().
- --create-owner CLI-флаг (main.rs) со скрытым вводом пароля (rpassword) —
вместо ручного SQL для создания первого владельца с VPS-консоли.
- src/modules/staff/ — эндпоинты создания модераторов/партнёров, переключения
can_manage_nodes, списка заявок на вывод и их approve/reject/paid.
- Комиссия партнёра начисляется в BillingService::confirm_payment отдельным
шагом после существующей рефералки, пересчитывается через
get_plan_price(invoice.plan_name, invoice.currency) — НЕ через
invoice.amount напрямую, потому что у провайдеров разная внутренняя
единица (TON — nanoTON+2% буфер, CryptoBot — фиатные центы напрямую).
- bot.rs — /start p_XXXXXX привязывает покупателя к партнёру по коду
(first-wins, до существующих веток numeric-tg_id-рефералки и кода входа).
- frontend/ — AdminLogin/AdminLayout/StaffPage/WithdrawalsPage/EarningsPage
поверх существующего Vite-админ-фронта (без отдельного проекта).
Оплата через @CryptoBot
------------------------
- modules/billing/providers/cryptobot.rs — новый PaymentProvider (Crypto Pay
API), плюс попутно исправлены два реальных бага в существующем коде:
confirm_payment был жёстко завязан на TON-провайдера при любом инвойсе, и
InvoiceRecord не хранил currency, из-за чего TON-курс молча применялся ко
всем провайдерам вместо родной валюты инвойса.
- bot.rs — экран выбора способа оплаты для CryptoBot-инвойсов.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Прокси теперь может валидировать клиентские JWT и отчитываться о расходе
трафика через новые internal-эндпоинты (защищены X-Internal-Secret), лимит
на юзера настраивается динамически через admin PATCH без передеплоя прокси.
Заодно реализован задокументированный, но не подключённый флоу входа через
Telegram-бота (magic-link) для десктоп/мобильного приложения, и seed-эндпоинты
теперь возвращают access_token в JSON (не только в cookie) — без этого
Tauri-приложение не могло ими пользоваться.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Auth.tsx and Profile.tsx read import.meta.env.VITE_BOT_USERNAME — a Vite
build-time constant. This ЛК frontend is a single static bundle built
once in CI and served identically by both prod and dev (no
server-rendering to re-inject a value per environment, unlike the
Next.js landing fix from earlier). So the Telegram Login Widget on the
dev stand was rendering with whatever bot the CI build happened to be
compiled with — in practice the fallback default, the prod bot's
username — and Telegram correctly refused it with "Bot domain invalid"
since that bot's registered domain is the prod domain, not
dev.netrunner-vpn.com.
Added GET /api/v1/config (unauthenticated, reads BOT_USERNAME from the
server's actual environment on every request) and switched both files to
fetch it at runtime instead of reading the baked-in env var. Verified
locally: the dev container's /api/v1/config correctly returns its own
dev bot's username, and a real headless Chrome run confirms the Login
Widget script tag gets that value, not the hardcoded fallback.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Session cookies were hardcoded Secure, which is correct for prod but
silently breaks every non-localhost HTTP dev deployment: browsers (and
any spec-compliant HTTP client, verified with curl against a container IP
and against dev.netrunner-vpn.com directly) refuse to store a Secure
cookie received over plain HTTP unless the host is localhost. The dev VPS
serves everything over plain HTTP with no reverse-proxy/TLS by design, so
every login there was silently failing to persist a session at all —
this is a bigger, more fundamental gap than the cookie-sharing-with-the-
landing question that led to finding it.
Added COOKIE_SECURE (defaults true) alongside the existing COOKIE_DOMAIN,
logs a loud warning when disabled, and turned it off for the dev-remote
and local docker-dev env templates. Verified end-to-end against a
non-localhost address that the cookie now actually gets stored and a
follow-up authenticated request succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Auth was effectively broken for every login path: the Telegram handler
built a Set-Cookie header but never attached it to the response, and
Ghost Protocol (seed) login never set a cookie at all — so billing, the
game, and admin actions never actually worked once the frontend was fixed
to stop trying to read an HttpOnly cookie from JS. Replaced the bare
10-minute JWT with AuthService::issue_session: a 15-minute access cookie
plus a rotating 30-day refresh cookie (opaque token, only its SHA-256
hash stored in the new refresh_sessions table); reusing an already-
rotated refresh token now revokes every session for that user. CSRF/CORS
origin allowlists moved from a single hardcoded domain to env-configured
lists so the same session works across the landing and account subdomains.
The bot's WebApp deep links (Личный Кабинет/Тарифы/Синдикат) now bridge
through a short-lived bootstrap token exchanged via POST /auth/exchange
instead of a bare access token in the URL.
Nodes: /nodes/routing now serializes the tunnel_* fields, public_key,
sni_domain and a one-time session token gated on an active subscription
instead of a stub ip/port/protocol list; node provisioning returns 202
immediately and finishes in the background instead of blocking the
request for the whole SSH run; a new background task TCP-pings nodes
every 60s and flips online/offline itself; admin can now force-restart a
node over SSH.
Billing: TON exchange rate is cached in Redis (60s) instead of hitting
TonAPI on every invoice, and the request/response now actually uses the
requested currency and TonAPI's real (uppercase) key casing — previously
only USD/RUB were ever requested and the lookup used the wrong case, so
non-USD/RUB plans could never price correctly.
Cyberhack: the server now generates and stores the board itself and
replays the client's raw click path to compute the score, instead of
clamping a client-reported number — closes the "final score is whatever
the browser sends" hole flagged in the security audit.
Frontend: api.ts no longer tries to read the HttpOnly session cookie from
JS (that never worked) and instead relies on same-origin credentials plus
a silent refresh-and-retry on 401; AdminDashboard's restart/delete actions
are wired up; Auth.tsx drops the artificial delays and requires an
explicit seed download/confirmation before continuing, since a lost Ghost
seed is unrecoverable.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>