Unify auth into refresh-token cookie sessions; server-authoritative nodes and Cyberhack

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>
This commit is contained in:
2026-07-04 15:29:51 +07:00
parent 588adbb92b
commit 52531dd41a
41 changed files with 2219 additions and 508 deletions
+22 -11
View File
@@ -9,19 +9,28 @@
- `GET /nodes` — список активных нод (`status = 'online'`), отсортирован по
возрастанию `current_load` — то есть новые юзеры естественным образом идут на самые
свободные сервера.
- `GET /nodes/stats` — агрегаты для лендинга (страна, загрузка, статус). **`uptime`
заглушка**, см. TODO в `controller.rs`.
- `GET /nodes/routing` — конфиг для клиентского приложения (Tauri/mobile). **Сейчас
неполный** — см. TODO в `controller.rs` и в корневом README.
- `GET /nodes/stats` — агрегаты для лендинга (страна, загрузка, статус, `last_seen`).
- `GET /nodes/routing` — полный `TunnelConfig` для клиентского приложения: `ip/port`,
`tunnel_address/prefix/dns/mtu`, `public_key`/`sni_domain` ноды и одноразовый (60с)
`session_token` (HMAC-подписанный, `NodeService::generate_session_token`) — `null`,
если у юзера нет активной подписки.
- `admin_router` (за `auth_guard` **и** `admin_guard`, в таком порядке — `admin_guard`
ожидает, что `auth_guard` уже положил юзера в extensions):
- `POST /admin/nodes`добавить и заprovisioning новую ноду.
- `GET /admin/nodes`список **всех** нод независимо от статуса (в отличие от
публичного `GET /nodes`, который отдаёт только `online`) — нужен админке, чтобы видеть
ноды в `provisioning`/`offline`.
- `POST /admin/nodes` — сразу вставляет ноду со статусом `provisioning` и отвечает
`202 Accepted`; реальный SSH-провижининг идёт в фоне (`spawn_provisioning`).
- `DELETE /admin/nodes/{id}` — удалить ноду из БД (саму VPS-машину это не трогает).
- `POST /admin/nodes/{id}/restart` — перезапускает прокси-контейнер на ноде по SSH
(уже по мастер-ключу, не по паролю — см. ниже).
## Провижининг (`service.rs::provision_node_via_ssh`)
## Провижининг (`service.rs`)
Синхронная (в смысле — блокирующий поток через `spawn_blocking`, но HTTP-запрос ждёт её
целиком, см. TODO) операция по `ssh2`:
`add_node` вставляет ноду в БД со статусом `provisioning` и сразу возвращает управление
контроллеру (`202 Accepted`); сам SSH-провижининг (`provision_node_via_ssh`) выполняется в
фоне (`tokio::spawn` + `spawn_blocking` для блокирующего `ssh2`), по завершении переводит
ноду в `online` (успех) или `offline` (провал, с логом причины):
1. Подключается к `ip:22` с таймаутом 15с, логинится по паролю root (временный пароль,
передаётся один раз при создании ноды).
@@ -30,9 +39,11 @@
3. Прописывает публичный ключ control plane (`keys/netrunner_master_ed25519.pub`,
создаётся заранее, см. `main.rs::ensure_master_ssh_key`) в `authorized_keys` и **жёстко
выключает парольный SSH-вход** (hardening-скрипт внутри метода) — после провижининга
зайти на ноду можно только по ключу.
зайти на ноду можно только по ключу. Именно поэтому `restart_node` подключается уже
по ключу (`connect_ssh_with_key`), а не по паролю, которого больше нет.
4. Не более 3 одновременных провижинингов (`Semaphore::new(3)`), чтобы не заDDoSить сам
control plane при массовом добавлении нод.
Одновременно с провижинингом только `add_vpn_node` пишет запись в БД — если SSH-шаг
упал, запись не создаётся вовсе (не остаётся полуживых нод в базе).
Health-check (`src/tasks.rs`, раз в `NODE_HEALTH_CHECK_INTERVAL` секунд, по умолчанию 60)
TCP-пингует каждую не-`provisioning` ноду и переоценивает `online`/`offline` сам — control
plane больше не может бесконечно долго считать мёртвую ноду живой.