Ролевая админка (Owner/Moderator/Partner) + оплата через @CryptoBot

Ролевая система вместо привязки к 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>
This commit is contained in:
2026-07-08 22:53:02 +07:00
parent c8eea8203e
commit 375b333978
31 changed files with 2607 additions and 115 deletions
+8 -3
View File
@@ -31,6 +31,7 @@ use crate::modules::{
nodes::{controller as node_ctrl, service::NodeService},
proxy_internal,
referrals::{controller as ref_ctrl, service::ReferralService},
staff::{controller as staff_ctrl, service::StaffService},
users::{controller as user_ctrl, service::UserService},
};
@@ -41,9 +42,9 @@ pub struct ApiState {
pub node_service: NodeService,
pub referral_service: ReferralService,
pub user_service: UserService,
pub staff_service: StaffService,
pub jwt_secret: String,
pub bot_token: String,
pub admin_tg_id: i64,
/// Юзернейм бота (без `@`) — отдаётся фронту через `GET /api/v1/config`.
/// Не публичный build-time env (`VITE_BOT_USERNAME`): фронт — статический
/// SPA-бандл, собираемый один раз в CI и одинаковый что для прод-, что для
@@ -82,9 +83,9 @@ impl Clone for ApiState {
node_service: self.node_service.clone(),
referral_service: self.referral_service.clone(),
user_service: self.user_service.clone(),
staff_service: self.staff_service.clone(),
jwt_secret: self.jwt_secret.clone(),
bot_token: self.bot_token.clone(),
admin_tg_id: self.admin_tg_id,
bot_username: self.bot_username.clone(),
cookie_domain: self.cookie_domain.clone(),
cookie_secure: self.cookie_secure,
@@ -139,7 +140,11 @@ pub fn create_router(state: ApiState, frontend_dir: &str) -> Router {
let admin_routes_v1 = Router::new()
.nest("/nodes", node_ctrl::admin_router(state.clone()))
.nest("/users", user_ctrl::admin_router(state.clone()));
.nest("/users", user_ctrl::admin_router(state.clone()))
.nest("/staff", staff_ctrl::router(state.clone()))
.nest("/staff", staff_ctrl::owner_router(state.clone()))
.nest("/staff", staff_ctrl::staff_router(state.clone()))
.nest("/partners", staff_ctrl::partner_router(state.clone()));
// Внутренний контракт для прокси-нод — отдельный секрет, не auth_guard
// (прокси не пользовательская сессия) и не участвует в CORS ниже (никогда