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:
+26
-14
@@ -7,18 +7,30 @@
|
||||
`expired`) + список рефералов одним JSON-объектом. Это осознанно сделано на стороне
|
||||
БД (`jsonb_build_object`/`jsonb_agg`), а не в Rust несколькими запросами.
|
||||
|
||||
## Cyberhack (мини-игра)
|
||||
## Cyberhack (мини-игра) — server-authoritative
|
||||
|
||||
- `POST /users/hack/result` (`{ score }`) — фронтенд (`frontend/src/HackGame.tsx`,
|
||||
используется npm-пакет `cyberhack`) присылает результат раунда игры.
|
||||
- `score` клампится на сервере в `0..=500` — это **единственная** серверная проверка
|
||||
честности результата; клиент фактически сам решает, сколько очков получить.
|
||||
Неэксплуатируемо, пока баланс E$ ни на что не тратится, но нужно закрыть до того, как
|
||||
появится обмен баланса на что-то ценное — см. `// TODO` в `controller.rs`.
|
||||
- `add_hack_reward` -> `repo.consume_ticket_and_reward` списывает один игровой билет и
|
||||
начисляет баланс **в одной транзакции с `SELECT ... FOR UPDATE`**, чтобы параллельные
|
||||
запросы одного юзера не списали билет дважды. Дополнительно требует минимум 45 секунд
|
||||
между попытками (`last_hack_at`) — защита от скриптового фарма очков.
|
||||
- Билеты выдаются только при покупке подписки (`billing::buy_subscription`, +1 билет,
|
||||
кэп 10) — то есть поиграть в Cyberhack без действующей/когда-либо купленной подписки
|
||||
нельзя.
|
||||
Вся игровая логика (генерация доски, проверка ходов, подсчёт очков) продублирована на
|
||||
сервере в [`cyberhack.rs`](cyberhack.rs) — порт 1:1 алгоритма из `Cyberhack/src/lib.rs`
|
||||
(WASM/Yew-движок игры). Раньше клиент сам генерировал доску, сам считал `score` и просто
|
||||
присылал готовое число (сервер лишь клампил его в `0..=500`) — это давало клиенту полный
|
||||
контроль над наградой.
|
||||
|
||||
Флоу теперь в два шага:
|
||||
|
||||
1. **`POST /users/hack/start`** — списывает билет (та же атомарная транзакция с
|
||||
`SELECT ... FOR UPDATE` + минимум 45 секунд между попытками, что и раньше, см.
|
||||
`repository::start_hack_session`), генерирует доску/цели на сервере, сохраняет их в
|
||||
`hack_sessions` (TTL = время игры + запас на сетевую задержку) и отдаёт клиенту
|
||||
`{session_id, matrix, targets, base_value, time_limit}`. Клиент (WASM-игра) обязан
|
||||
играть строго на этой доске, не генерировать свою.
|
||||
2. **`POST /users/hack/result`** (`{ session_id, path }`) — `path` это сырая
|
||||
последовательность кликов `(row, col)` в порядке совершения, а не итоговый счёт.
|
||||
`repository::consume_hack_session` атомарно помечает сессию использованной (повторная
|
||||
сдача — отказ) и возвращает сохранённую доску; `cyberhack::replay_path` реплеит путь,
|
||||
проверяя легальность каждого хода (чередование строка/столбец, без повторных клеток,
|
||||
лимит буфера), и сам считает, какие цели реально выполнены. Награда начисляется по
|
||||
этому результату — то, что мог бы прислать клиент, нигде не используется.
|
||||
|
||||
Билеты выдаются только при покупке подписки (`billing::buy_subscription`, +1 билет,
|
||||
кэп 10) — то есть поиграть в Cyberhack без действующей/когда-либо купленной подписки
|
||||
нельзя.
|
||||
|
||||
Reference in New Issue
Block a user