52531dd41a
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>
4.0 KiB
4.0 KiB
modules/billing — тарифы, инвойсы, оплата
Поток оплаты
POST /billing/pay/initiate(initiate_payment) — сервер сам смотрит цену тарифа в нужной валюте (plan_prices, см.migrations/0001_init.sql), берёт курс TON (сначала кеш в Redis —ton_rate:<currency>, TTL 60с; на промах — TonAPI, ответ кладётся в кеш), считает сумму в nanoTON с буфером +2% (calculate_ton_invoice— компенсация волатильности курса между генерацией инвойса и отправкой транзакции юзером) и создаётinvoices-запись со статусомpending. Клиент не может повлиять на сумму — она целиком считается на сервере. Запрос к TonAPI идёт по коду валюты из инвойса (не захардкожен наusd,rub), а ответ разбирается по ключу в ВЕРХНЕМ регистре (prices.USD) — так его и отдаёт TonAPI.- Фронтенд получает
{ destination, amount, comment }и просит кошелёк (TonConnect) отправить транзакцию с этимcomment(=invoice_id) — по нему инвойс потом находится. POST /billing/pay/confirm(confirm_payment) — клиент присылаетtx_hashотправленной транзакции. Сервер:- проверяет, что инвойс принадлежит вызывающему юзеру (иначе можно было бы дёргать
чужие
invoice_idи провоцировать обращения к блокчейну — DoS/перебор); - идемпотентен: повторный вызов на уже
paidинвойсе — no-op; - просит
TonProvider::verify_paymentсходить в блокчейн и сверить сумму (>=), адрес назначения и комментарий транзакции по хешу; - при успехе переводит инвойс в
paid, активирует подписку (buy_subscription, заодно выдаёт один игровой билет, до кэпа в 10) и начисляет рефереру 10% от суммы инвойса (reward_referrer).
- проверяет, что инвойс принадлежит вызывающему юзеру (иначе можно было бы дёргать
чужие
- Фоновая сверка (
reconcile_pending_payments, гоняется изtasks.rs) находитpending-инвойсы младше суток, чьи транзакции уже видны в истории мастер-кошелька (по совпадениюcomment == invoice_id), и доводит их доpaid— на случай, если клиент не смог/не успел вызвать/pay/confirmсам.
Провайдеры (providers/)
PaymentProvider — трейт, чтобы позже добавить Stripe/YooKassa, не трогая
BillingService. Сейчас единственная реализация — [ton::TonProvider] (HTTP к TonAPI,
без собственного состояния кроме адреса мастер-кошелька).
Триал
activate_trial доступен только аккаунтам с tg_id (см. auth/README.md)
и только один раз (has_used_trial).
Известные ограничения
Пересборка providers (включая повторное чтение env) при каждом Clone сервиса —
axum клонирует ApiState, а с ним и BillingService, на каждый HTTP-запрос; см. // TODO прямо в service.rs. Курс TON теперь кешируется (см. выше), это ограничение — нет.