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>
3.0 KiB
db/ — доступ к данным
models.rs— табличные структуры (#[derive(FromRow)]:User,Subscription,VpnNode,PromoCode,RefreshSession) и производные DTO, которые уходят напрямую в JSON API (ProfileData,SubscriptionData,ReferralData).repository.rs— трейт [AppRepository] (единая граница междуmodules::*::serviceи Postgres) и его единственная реализацияPgRepository. Сервисы модулей зависят только от трейта (Arc<dyn AppRepository>), не от конкретной СУБД.
Почему трейт, а не просто PgPool напрямую в сервисах
AppRepository — это явная граница: каждый метод — одна осмысленная доменная операция
(consume_ticket_and_reward, apply_promo_code, buy_subscription), а не сырой SQL,
размазанный по сервисам. Это также даёт возможность подменить реализацию в тестах без
БД, хотя на практике сейчас (см. tests/db_integration_test.rs) тесты гоняются через
#[sqlx::test] на реальном (эфемерном) Postgres, а не через мок трейта.
Транзакционные инварианты
Там, где над данными возможна гонка при параллельных запросах, используется
pool.begin() + явная блокировка строки:
| Операция | Защита |
|---|---|
start_hack_session (билет Cyberhack) |
SELECT ... FOR UPDATE по юзеру |
consume_hack_session (сдача результата) |
UPDATE ... WHERE used_at IS NULL RETURNING — повторная сдача одной сессии невозможна |
apply_promo_code |
SELECT ... FOR UPDATE по промокоду + UNIQUE(promo_id, user_id) |
reward_referrer |
UPDATE ... RETURNING атомарно проверяет и переводит статус реферала |
| Инвойсы | invoices.external_id — UNIQUE, поэтому один tx-хеш нельзя засчитать дважды |
refresh_session (ротация refresh-токена) |
UPDATE ... WHERE revoked_at IS NULL — повторное использование отозванного токена отзывает все сессии юзера |
Модели, для которых важно помнить о NULL
User.tg_id и User.seed_hash — взаимоисключающе опциональны: обычный юзер имеет
tg_id, анонимный Ghost-юзер — seed_hash. Код, который трактует отсутствие tg_id
как «анонимный юзер» (например, запрет триала), находится в
billing::service::activate_trial.