375b333978
Ролевая система вместо привязки к 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>
69 lines
4.4 KiB
SQL
69 lines
4.4 KiB
SQL
-- =====================================================================
|
||
-- РОЛЕВАЯ АДМИНКА: 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);
|