Ролевая админка (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:
+53
-8
@@ -18,7 +18,7 @@ use netrunner_control_plane::bot;
|
||||
use netrunner_control_plane::db::repository::{AppRepository, PgRepository};
|
||||
use netrunner_control_plane::modules::{
|
||||
auth::service::AuthService, billing::service::BillingService, nodes::service::NodeService,
|
||||
referrals::service::ReferralService, users::service::UserService,
|
||||
referrals::service::ReferralService, staff::service::StaffService, users::service::UserService,
|
||||
};
|
||||
use netrunner_control_plane::tasks;
|
||||
|
||||
@@ -27,6 +27,12 @@ use netrunner_control_plane::tasks;
|
||||
struct Args {
|
||||
#[arg(long)]
|
||||
migrate: bool,
|
||||
/// Одноразовое создание первого Owner-аккаунта (логин+пароль, не
|
||||
/// Telegram) — прогоняется руками на VPS: `--create-owner --username admin`.
|
||||
#[arg(long)]
|
||||
create_owner: bool,
|
||||
#[arg(long)]
|
||||
username: Option<String>,
|
||||
}
|
||||
|
||||
fn init_tracing() {
|
||||
@@ -149,6 +155,49 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Бутстрап первого Owner'а — тоже требует только DATABASE_URL, тот же
|
||||
// one-shot паттерн, что и --migrate. Пароль вводится скрыто в консоли
|
||||
// (не через env/аргумент), чтобы не осесть в истории шелла.
|
||||
if args.create_owner {
|
||||
let username = args
|
||||
.username
|
||||
.clone()
|
||||
.ok_or("--create-owner требует --username <имя>")?;
|
||||
info!("🔐 Создание владельца (Owner): {}", username);
|
||||
|
||||
let password = rpassword::prompt_password("Пароль для владельца: ")?;
|
||||
let password_confirm = rpassword::prompt_password("Повторите пароль: ")?;
|
||||
if password != password_confirm {
|
||||
return Err("Пароли не совпадают.".into());
|
||||
}
|
||||
if password.len() < 8 {
|
||||
return Err("Пароль должен быть не короче 8 символов.".into());
|
||||
}
|
||||
|
||||
let hash = AuthService::hash_password(&password)
|
||||
.map_err(|e| format!("Ошибка хеширования пароля: {:?}", e))?;
|
||||
|
||||
let repo = PgRepository::new(pool.clone());
|
||||
let user = repo
|
||||
.create_staff_user(&username, &hash, "owner", false, None, None)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if let sqlx::Error::Database(db_err) = &e {
|
||||
if db_err.constraint() == Some("users_username_key") {
|
||||
return format!("Логин '{}' уже занят.", username);
|
||||
}
|
||||
}
|
||||
format!("Ошибка создания владельца: {e}")
|
||||
})?;
|
||||
|
||||
info!("✅ Владелец создан: id={}, username={}", user.id, username);
|
||||
println!(
|
||||
"Владелец '{}' успешно создан. Войдите через /admin/login на фронтенде.",
|
||||
username
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// --- Полный запуск сервера: проверяем все обязательные ресурсы ---
|
||||
ensure_master_ssh_key()?;
|
||||
|
||||
@@ -174,12 +223,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
std::env::var("FRONTEND_DIR").expect("КРИТИЧЕСКАЯ ОШИБКА: FRONTEND_DIR не задан в .env!");
|
||||
let port = std::env::var("PORT").expect("КРИТИЧЕСКАЯ ОШИБКА: PORT не задан в .env!");
|
||||
|
||||
// Считываем ADMIN_TG_ID один раз при старте сервера
|
||||
let admin_tg_id = std::env::var("ADMIN_TG_ID")
|
||||
.expect("КРИТИЧЕСКАЯ ОШИБКА: ADMIN_TG_ID не задан в .env!")
|
||||
.parse::<i64>()
|
||||
.expect("ADMIN_TG_ID должен быть числом");
|
||||
|
||||
info!("🚀 Запуск Netrunner Control Plane v2.0 (Modular Architecture)");
|
||||
|
||||
// Домен для Set-Cookie: пусто локально (host-only cookie), в проде
|
||||
@@ -252,9 +295,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
node_service: NodeService::new(repo.clone()),
|
||||
referral_service: ReferralService::new(repo.clone()),
|
||||
user_service: UserService::new(repo.clone()),
|
||||
staff_service: StaffService::new(repo.clone()),
|
||||
jwt_secret,
|
||||
bot_token: bot_token.clone(),
|
||||
admin_tg_id,
|
||||
bot_username,
|
||||
cookie_domain,
|
||||
cookie_secure,
|
||||
@@ -279,6 +322,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let bot_auth = state.auth_service.clone();
|
||||
let bot_users = state.user_service.clone();
|
||||
let bot_billing = state.billing_service.clone();
|
||||
let bot_referrals = state.referral_service.clone();
|
||||
let bot_token_for_task = bot_token.clone();
|
||||
|
||||
let bot_handle = tokio::spawn(async move {
|
||||
@@ -289,6 +333,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
bot_auth,
|
||||
bot_users,
|
||||
bot_billing,
|
||||
bot_referrals,
|
||||
bot_cancel_token,
|
||||
)
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user