Commit Graph

56 Commits

Author SHA1 Message Date
nineap e6c81bf8fd Дублирует пайплайны для Gitea Actions (.gitea/workflows/)
Build & Push Image / Build and push to GHCR (push) Failing after 3m1s
Build & Push Image / Build and push Caddy (Cloudflare DNS) image (push) Failing after 11s
CI / fmt + clippy + test (push) Failing after 21m4s
Копии .github/workflows/{ci,build,deploy}.yml — Gitea Actions синтаксически
совместим с GitHub Actions, поэтому большая часть 1:1. Отличие: логин на
ghcr.io в build.yml теперь через secrets.GHCR_TOKEN (настоящий GitHub PAT),
не secrets.GITHUB_TOKEN — автотокен Gitea валиден только для самого Gitea,
на ghcr.io им не залогиниться. deploy.yml уже использовал GHCR_TOKEN,
не тронут в этой части.

Нужно завести в Gitea: секрет GHCR_TOKEN (write:packages), плюс все
остальные секреты, что уже есть в GitHub Actions (отдельное хранилище,
не шарится автоматически). Триггер workflow_run в deploy.yml поддержан не
во всех версиях Gitea Actions — см. комментарий в файле.
2026-07-09 14:28:05 +07:00
nineap 7290f9483b Тесты для ключевой бизнес-логики + 3 найденных бага
Добавляет 52 теста (было 7), приоритет: биллинг -> auth -> RBAC-гварды ->
staff/выводы средств -> hack-сессии -> рефералы -> proxy_internal.

Попутно найдены и исправлены реальные баги:
- migrations/0004: ALTER TABLE auth_sessions падал на чистой БД, потому что
  0002 эту таблицу уже дропнул — ломало --migrate на CI/новом инстансе и
  весь sqlx::test. Заменено на CREATE TABLE IF NOT EXISTS.
- request_withdrawal: гонка при параллельных заявках на вывод (не было
  FOR UPDATE) — партнёр мог одновременно вывести баланс кратно больше
  доступного. Новый repository::request_partner_withdrawal блокирует
  строку партнёра и пересчитывает остаток в одной транзакции.
- get_referral_stats: SUM() над BIGINT возвращал NUMERIC, sqlx падал на
  ColumnDecode для любого юзера с рефералами.
- reward_referrer: начислял рефереру 10% при каждой повторной покупке
  того же приведённого юзера вместо разового бонуса — добавлен фильтр
  WHERE status = 'pending'.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 01:18:15 +07:00
nineap 375b333978 Ролевая админка (Owner/Moderator/Partner) + оплата через @CryptoBot
Ролевая система вместо привязки к 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>
2026-07-08 22:53:02 +07:00
nineap c8eea8203e Добавить проверку токена прокси, динамические лимиты трафика и Telegram magic-link вход
Прокси теперь может валидировать клиентские JWT и отчитываться о расходе
трафика через новые internal-эндпоинты (защищены X-Internal-Secret), лимит
на юзера настраивается динамически через admin PATCH без передеплоя прокси.
Заодно реализован задокументированный, но не подключённый флоу входа через
Telegram-бота (magic-link) для десктоп/мобильного приложения, и seed-эндпоинты
теперь возвращают access_token в JSON (не только в cookie) — без этого
Tauri-приложение не могло ими пользоваться.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 00:30:25 +07:00
nineap 1a26403afb Переключить dev Caddy на порт 443 вместо 8443
Telegram Login Widget в браузере требует именно порт 443 — его CSP
frame-ancestors от oauth.telegram.org не учитывает порт домена и всегда
подразумевает дефолтный порт схемы, поэтому на 8443 виджет рендерился, но
iframe блокировался как cross-origin. VPN-нода, ранее занимавшая 443 на
dev VPS, перенесена на другой порт, так что конфликта больше нет.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 20:00:09 +07:00
nineap 3af0c46130 docs: reflect runtime bot_username config in frontend README
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 14:23:31 +07:00
nineap 818d4fa384 Serve bot_username at runtime instead of baking it into the SPA build
Auth.tsx and Profile.tsx read import.meta.env.VITE_BOT_USERNAME — a Vite
build-time constant. This ЛК frontend is a single static bundle built
once in CI and served identically by both prod and dev (no
server-rendering to re-inject a value per environment, unlike the
Next.js landing fix from earlier). So the Telegram Login Widget on the
dev stand was rendering with whatever bot the CI build happened to be
compiled with — in practice the fallback default, the prod bot's
username — and Telegram correctly refused it with "Bot domain invalid"
since that bot's registered domain is the prod domain, not
dev.netrunner-vpn.com.

Added GET /api/v1/config (unauthenticated, reads BOT_USERNAME from the
server's actual environment on every request) and switched both files to
fetch it at runtime instead of reading the baked-in env var. Verified
locally: the dev container's /api/v1/config correctly returns its own
dev bot's username, and a real headless Chrome run confirms the Login
Widget script tag gets that value, not the hardcoded fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 14:21:55 +07:00
nineap 4f1efc6ff9 Fix infinite reload loop on the login screen itself
apiFetch()'s fallback for a failed silent-refresh unconditionally does
window.location.href = "/" — but Auth.tsx (the "/" route) calls this same
apiFetch("/users/me") on mount just to check whether a session already
exists. For any fresh/anonymous visit that 401s (the normal, expected
case), the failed-refresh branch fired a navigation to "/" while already
sitting on "/" — which browsers still treat as a real reload, not a
no-op. That reload remounts the app, which repeats the exact same check,
which fails the exact same way: an infinite reload loop, observed live as
the page "loading forever" and — since the bot's WebApp bridge lands on
/profile and bounces to "/" on any auth hiccup — the "Личный Кабинет"
button appearing to do nothing.

Only redirect when not already on "/". Verified with a real headless
Chrome run (Playwright): before, this path can't be distinguished from a
one-shot failure without watching for repeated navigation; confirmed here
that with the guard in place the app performs exactly one GET /users/me +
one POST /auth/refresh and then sits idle, no repeat requests or
navigations over 12s of observation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 14:03:45 +07:00
nineap 4a8816b908 Build the Caddy image in CI instead of on the dev VPS
The previous deploy attempt confirmed the failure mode directly: `docker
compose build caddy` over SSH got most of the way through compiling
xcaddy (downloading Go modules) and then the whole SSH command hit
appleboy/ssh-action's timeout mid-build, aborting the deploy before it
ever reached `up -d`. Compiling anything on that VPS was already a
deliberate anti-pattern here — see DEPLOYMENT.md's "compilation happens
in GitHub Actions, never on the VPS" — this just hadn't been applied to
the new Caddy service yet.

Added a build-caddy job to build.yml pushing
ghcr.io/nineap/netrunner-caddy-cloudflare:latest (hardcoded lowercase
owner — github.repository_owner resolves to "nineAp" and Docker rejects
mixed-case image refs, same fix already applied to the main image's
deploy script). docker-compose.dev-remote.yml now just pulls that image;
deploy.yml no longer builds it over SSH or ships Dockerfile.caddy to the
server at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 13:52:07 +07:00
nineap d22cd0de41 Add Caddy for real TLS on dev — Telegram Web Apps require https://
Bot buttons that open a Web App (Личный Кабинет/Тарифы/Синдикат) appeared
to load and then silently do nothing on the dev stand. Root cause:
teloxide's WebAppInfo::url (and the underlying Bot API) requires an HTTPS
URL — Telegram's client refuses to render a Web App served over plain
HTTP, with no error surfaced to the user, just a stuck loading state.
WEB_APP_BASE_URL was http://dev.netrunner-vpn.com:8080.

Port 443 on the dev VPS is already in use by a VPN node, and port 80 is
closed, so a normal HTTP-01 ACME challenge won't work. Added a custom
Caddy build (caddy-dns/cloudflare) terminating TLS via DNS-01 on port
8443 instead, reverse-proxying to the existing plain-HTTP app container.
WEB_APP_BASE_URL and the CORS/CSRF allowlists now point at the :8443
origin, and COOKIE_SECURE reverts to true (the default, matching prod) —
now that the backend actually has real TLS to serve it over, we no
longer need the plain-HTTP workaround introduced for the earlier
cookie-drop bug. Verified the custom image builds and the Caddyfile
validates (only the dummy Cloudflare token used for the local test is
rejected, as expected).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 19:36:38 +07:00
nineap acfcdbe90e Make the Secure cookie attribute configurable for TLS-less dev stands
Session cookies were hardcoded Secure, which is correct for prod but
silently breaks every non-localhost HTTP dev deployment: browsers (and
any spec-compliant HTTP client, verified with curl against a container IP
and against dev.netrunner-vpn.com directly) refuse to store a Secure
cookie received over plain HTTP unless the host is localhost. The dev VPS
serves everything over plain HTTP with no reverse-proxy/TLS by design, so
every login there was silently failing to persist a session at all —
this is a bigger, more fundamental gap than the cookie-sharing-with-the-
landing question that led to finding it.

Added COOKIE_SECURE (defaults true) alongside the existing COOKIE_DOMAIN,
logs a loud warning when disabled, and turned it off for the dev-remote
and local docker-dev env templates. Verified end-to-end against a
non-localhost address that the cookie now actually gets stored and a
follow-up authenticated request succeeds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 19:20:14 +07:00
nineap a7607e4b33 dev-remote: use dev.netrunner-vpn.com instead of a bare IP for Telegram
Telegram Login Widget refuses to run on a raw IP ("Bot domain invalid")
— BotFather's /setdomain only accepts an actual domain name. Switched
WEB_APP_BASE_URL and the landing origin in CORS/CSRF allowlists to
dev.netrunner-vpn.com (DNS A-record -> 45.76.161.137), and called out
explicitly that the dev bot must be a separate BotFather bot from prod —
reusing the prod bot's username on this host was guaranteed to fail since
its domain is already registered to the real prod domain.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 18:48:51 +07:00
nineap 0927053f4a Fix Docker build: vendor the cyberhack wasm bundle instead of a cross-repo file: path
CI build failed with ENOENT on /Cyberhack/pkg: package.json pointed the
"cyberhack" dependency at "file:../../Cyberhack/pkg", a path outside this
repo's own directory tree entirely. That only ever resolved on a
developer machine that happened to have a sibling checkout of the
Cyberhack repo — Docker's build context is confined to this repo, and CI
never checks out Cyberhack in the first place, so pnpm install had
nothing to link against.

There's no npm registry publish set up for that package, so vendor the
already-built wasm-pack output directly into frontend/vendor/cyberhack
(committed) and point the dependency at "file:./vendor/cyberhack" instead
— a path inside the build context. Dockerfile now copies frontend/vendor
before pnpm install so the file: resolution actually has something to
link to. Verified with a `docker build --no-cache` end-to-end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:45:05 +07:00
nineap 52531dd41a 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>
2026-07-04 15:29:51 +07:00
nineap 588adbb92b fix: lowercase GHCR image path, drop broken SSH env-forwarding in prod deploy too
github.repository_owner resolves to "nineAp" (mixed case), which Docker
rejects as an invalid repository reference — hardcode the lowercase
"nineap" used by the GHCR package and the compose files' image default.
Also apply the same secrets-interpolation fix from the dev job to
deploy-prod so a future manual prod deploy doesn't hit the same
"password is empty" failure from unforwarded SSH env vars.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 12:55:59 +07:00
nineap 7da6e341d1 fix: stop relying on SSH env-var forwarding for GHCR_TOKEN in dev deploy
appleboy/ssh-action's envs: mechanism forwards vars via SSH SendEnv,
which sshd silently drops unless AcceptEnv is configured server-side.
On the freshly bootstrapped dev VPS that left GHCR_TOKEN empty on the
remote end, so `docker login --password-stdin` failed with "password
is empty" even though the SSH connection itself was fine. Interpolate
the secret directly into the script text instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 12:48:54 +07:00
nineap 1a6fad3714 fix: declare packages field in frontend pnpm-workspace.yaml
pnpm@9 (pinned in the prod Dockerfile) requires an explicit packages
field once pnpm-workspace.yaml exists, unlike newer pnpm versions
which tolerate it being absent. Without it, `pnpm build` fails with
"packages field missing or empty" during the Docker image build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 16:02:24 +07:00
nineap 465570267d ci: split deploy pipeline into dev (auto) and prod (manual) jobs
Add slim docker-compose.dev-remote.yml (app+db+redis, no monitoring)
for the remote dev VPS, and a deploy-dev job that auto-runs after a
successful image build on main. Bump Dockerfile.dev's Rust image so
edition2024 dependencies build. Stop tracking .env.dev (real secrets
were committed) in favor of .env.dev.example / .env.dev-remote.example
templates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 15:56:16 +07:00
Kirill d4a57c0b6a docs 2026-07-02 23:13:13 +07:00
Kirill e997a20fe6 deployment 2026-06-29 18:05:33 +07:00
Kirill f06cc48763 security: fix CSRF/ownership/timing vulns; add billing reconciliation
- auth/guard: добавить CSRF-проверку Origin/Referer для cookie-мутаций,
  исправить инвертированную логику admin_guard (пускал всех, блокировал админов),
  перевести admin_guard на from_fn_with_state
- auth/service: timing-safe сравнение HMAC через verify_slice вместо != по hex;
  криптостойкий Seed через Alphanumeric (был цифровой 0-9)
- billing: confirm_payment проверяет owner (caller_id == invoice.user_id);
  активация триала только для Telegram-аккаунтов (Sybil-защита);
  реализована reconcile_pending_payments — сверка blockchain->pending-инвойсы
- billing/providers: get_recent_transactions перенесён в трейт PaymentProvider
- nodes/service: unwrap() -> map_err по всему SSH-пути; убран TcpStream import
- docker: multi-stage build с cargo-chef, non-root user, healthcheck;
  prod-compose добавляет one-shot migrate-сервис, убирает проброс порта БД
- deploy.yml: параметр image_tag, set -e, pull всего стека вместо только app
- users/service: обработка NO_TICKETS / NO_USER из БД вместо catch-all

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 17:58:24 +07:00
nineap 26d6f2db8a deploy change, update token system, fix abuses and add admin verify 2026-05-26 12:50:25 +07:00
nineap 1ef93c2025 [AI] fix some modules, develop the big part of modules. TODO fix critical errors, update pipeline, test everything 2026-05-16 19:16:50 +07:00
nineap 0824033602 backend remove scripts 2026-05-15 17:47:08 +07:00
nineap 693e48af59 backend sliced by modules 2026-05-02 18:24:57 +07:00
nineap b6306d5aa9 cache problems fix 2026-04-28 19:55:11 +07:00
nineap cf85fbdd73 build fix 2026-04-28 19:46:46 +07:00
nineap b3543c8fac deploy update 2026-04-28 19:43:57 +07:00
nineap eebe10967d game update 2026-04-28 19:41:31 +07:00
nineap 1a759d1752 update and fixes 2026-04-28 18:17:25 +07:00
nineap fb88fd34a8 update and fixes 2026-04-28 18:08:56 +07:00
nineap 2e673a18ba deploy update 2026-04-28 16:19:38 +07:00
nineap 9b4190f20e pipeline update 2026-04-28 16:07:40 +07:00
nineap 70a789ea17 compose update 2026-04-28 16:03:32 +07:00
nineap 3a9c3b59ca pipeline update 2026-04-28 15:27:14 +07:00
nineap bf210971f2 refactoring 2026-04-28 15:20:58 +07:00
nineap f60c1d60c6 compose update 2026-04-25 19:12:09 +07:00
nineap 03d3a072d3 api update 2026-04-25 18:21:24 +07:00
nineap 3f635a81dc account serve fixes 2026-04-25 16:07:19 +07:00
nineap 19ac192866 api fix 2026-04-25 15:09:09 +07:00
nineap 5ffc4a34a7 auth update 2026-04-25 14:56:48 +07:00
nineap 17a4980329 errors fix 2026-04-24 17:01:57 +07:00
nineap 0dc6562dd2 trial log, profile fixes 2026-04-24 16:11:56 +07:00
nineap 24e3a6de44 trial updates 2026-04-24 15:51:03 +07:00
nineap 808c07855c maybe fix database and dashboard 2026-04-24 15:36:51 +07:00
nineap 75312d035a api fixes 2026-04-24 15:06:54 +07:00
nineap 05024fcf99 migrations update 2026-04-24 14:49:25 +07:00
nineap 40f7de803b subs and fixes hack game 2026-04-23 15:35:12 +07:00
nineap 8c33f5c097 game update test 2026-04-23 15:13:29 +07:00
nineap 5aecd41d4f update pipeline, create cache, optimized build 2026-04-23 14:50:36 +07:00