Files
netrunner-backend/migrations/0005_roles_and_partners.sql
T
nineap 375b333978 Ролевая админка (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>
2026-07-08 22:53:02 +07:00

69 lines
4.4 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- =====================================================================
-- РОЛЕВАЯ АДМИНКА: Owner / Moderator / Partner
-- =====================================================================
-- Раньше единственным способом получить админ-доступ была привязка к
-- ADMIN_TG_ID (см. AuthGuard::admin_guard) — один захардкоженный Telegram-
-- аккаунт на весь деплой. Теперь роль полноценно хранится в БД, у
-- Owner/Moderator/Partner есть логин+пароль (не Telegram/seed), а
-- ADMIN_TG_ID выпиливается из кода вместе с этой миграцией.
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_role_check;
ALTER TABLE users
ADD CONSTRAINT users_role_check
CHECK (role IN ('user', 'owner', 'moderator', 'partner', 'admin'));
-- 'admin' оставлен в допустимых значениях только ради обратной
-- совместимости с уже существующими строками — новый код его не
-- присваивает нигде.
ALTER TABLE users ADD COLUMN IF NOT EXISTS username VARCHAR(50) UNIQUE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash TEXT;
-- Право модератора управлять нодами — переключается Owner'ом поштучно,
-- а не зашито в саму роль (см. AuthGuard::node_manage_guard).
ALTER TABLE users ADD COLUMN IF NOT EXISTS can_manage_nodes BOOLEAN NOT NULL DEFAULT FALSE;
-- Фиксированный процент партнёра с продаж приведённых им покупателей.
ALTER TABLE users ADD COLUMN IF NOT EXISTS commission_percent NUMERIC(5,2);
-- Короткий код для диплинка t.me/<bot>?start=<partner_code> — префикс "p_"
-- нужен боту, чтобы однозначно отличать партнёрский код от numeric
-- referrer-tg_id и от auth_sessions.auth_code (см. bot.rs::Command::Start).
ALTER TABLE users ADD COLUMN IF NOT EXISTS partner_code VARCHAR(20) UNIQUE;
-- Постоянная привязка покупателя к партнёру, выставляется один раз при
-- регистрации через диплинк (first-wins, см. set_referred_by_partner).
ALTER TABLE users ADD COLUMN IF NOT EXISTS referred_by_partner_id UUID REFERENCES users(id);
CREATE INDEX IF NOT EXISTS idx_users_referred_by_partner ON users(referred_by_partner_id)
WHERE referred_by_partner_id IS NOT NULL;
-- Леджер начислений — по одной записи на оплаченный инвойс приведённого
-- покупателя, а не бегущий итог на одной строке (нужна история для вывода
-- средств). Валюта — валюта самого инвойса, без конвертации (см. BillingService::confirm_payment).
CREATE TABLE IF NOT EXISTS partner_commissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
partner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
currency VARCHAR(10) NOT NULL,
amount BIGINT NOT NULL,
percent_at_time NUMERIC(5,2) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(invoice_id) -- защита от повторного начисления при retry confirm_payment
);
CREATE INDEX IF NOT EXISTS idx_partner_commissions_partner_currency
ON partner_commissions(partner_id, currency);
CREATE TABLE IF NOT EXISTS withdrawal_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
partner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
currency VARCHAR(10) NOT NULL,
amount BIGINT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'approved', 'rejected', 'paid')),
note TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
processed_at TIMESTAMPTZ,
processed_by UUID REFERENCES users(id)
);
CREATE INDEX IF NOT EXISTS idx_withdrawal_requests_partner_status
ON withdrawal_requests(partner_id, status);
CREATE INDEX IF NOT EXISTS idx_withdrawal_requests_status ON withdrawal_requests(status);