feat(account): VK ID web login to link a VK identity from a browser
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 1m4s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s

A browser has no signed VK Mini App launch params, so linking VK on the web uses
VK ID's raw OAuth 2.1 flow (PKCE, no @vkid/sdk): the SPA redirects to VK's hosted
login and returns with an authorization code, which the gateway exchanges
server-side (confidential, under the VK "Web" app's protected key) for the trusted
vk user id — then the existing link/merge machinery attaches or merges it.

- fbs LinkVKRequest{code, device_id, code_verifier}; codec + TS bindings.
- backend link.Service ConfirmVK/MergeVK/attachVK (KindVK, mirror Telegram),
  handleLinkVK[Merge], routes /user/link/vk[/merge], backendclient LinkVK[Merge].
- gateway internal/vkid confidential code exchange (id.vk.com/oauth2/auth);
  transcode link.vk.confirm/merge (registered only when configured) + config
  GATEWAY_VK_ID_{APP_ID,CLIENT_SECRET,REDIRECT_URL} + main wiring.
- UI lib/vkid (PKCE authorize redirect + callback), Profile "Link VK" control,
  boot callback handling; a merge re-authorizes for a fresh code (VK codes are
  single-use). Web-only (a redirect strands a Mini App webview).
- Deploy: VITE_VK_APP_ID + VITE_VK_ID_REDIRECT_URL build args + gateway env,
  ci.yaml/prod-deploy TEST_/PROD_ vars, compose/Dockerfile/.env.example/README.
- Tests: vkid exchange unit (string/number user_id, id_token fallback, errors),
  transcode link.vk, backend ConfirmVK/MergeVK inttest, codec encodeLinkVK.
- Docs: ARCHITECTURE §4, FUNCTIONAL(+ru), gateway README.
This commit is contained in:
Ilia Denisov
2026-07-03 17:59:33 +02:00
parent 60faa4f064
commit 2c465c01d2
36 changed files with 1131 additions and 16 deletions
+6
View File
@@ -68,6 +68,8 @@ VITE_TELEGRAM_BOT_ID=
VITE_TELEGRAM_LINK= # friend-invite Mini App link (full URL, e.g. https://t.me/<bot>/<app>)
VITE_TELEGRAM_GAME_CHANNEL_NAME= # landing "Play in Telegram" link, the bot's game channel
VITE_VK_APP_LINK= # landing "Play on VK" link (full URL, https://vk.com/app<id>)
VITE_VK_APP_ID= # VK ID "Web" app id (client_id) for VK web-login linking; also the gateway's GATEWAY_VK_ID_APP_ID
VITE_VK_ID_REDIRECT_URL= # VK ID trusted redirect URL (e.g. https://erudit-game.ru/app/); also the gateway's exchange redirect_uri
VITE_GATEWAY_URL=
# --- Grafana ----------------------------------------------------------------
@@ -97,3 +99,7 @@ TELEGRAM_API_BASE_URL=
# launch-parameter signature in-process under it (a pure offline HMAC, no VK API call).
# Empty disables the VK auth path (auth.vk). Set from the Gitea TEST_/PROD_ secret.
GATEWAY_VK_APP_SECRET=
# VK ID web login (browser VK-identity linking) — a SEPARATE VK "Web" app: the gateway runs
# the confidential OAuth 2.1 code exchange under this protected key. The app id + redirect URL
# come from VITE_VK_APP_ID / VITE_VK_ID_REDIRECT_URL above. All three empty disables link.vk.*.
GATEWAY_VK_ID_CLIENT_SECRET=
+5 -2
View File
@@ -95,6 +95,9 @@ without it Docker's resolver handles `otelcol`, `gateway` and `api.telegram.org`
| `VITE_TELEGRAM_LINK` | variable | _(empty)_ | UI build-arg: friend-invite Mini App link (full URL, `https://t.me/<bot>/<app>``<app>` is the Mini App short name from BotFather). |
| `VITE_TELEGRAM_GAME_CHANNEL_NAME` | variable | _(empty)_ | UI build-arg: the landing "Play in Telegram" link, the bot's game channel (e.g. `https://t.me/Erudit_Game`). |
| `VITE_VK_APP_LINK` | variable | _(empty)_ | UI build-arg: the landing "Play on VK" link, the VK Mini App (full URL, `https://vk.com/app<id>`). |
| `VITE_VK_APP_ID` | variable | _(empty)_ | VK ID "Web" app id (`client_id`) for VK web-login linking. Baked into the SPA authorize URL and reused as the gateway's `GATEWAY_VK_ID_APP_ID`. Empty disables the `link.vk.*` ops. |
| `VITE_VK_ID_REDIRECT_URL` | variable | _(empty)_ | VK ID trusted redirect URL (e.g. `https://erudit-game.ru/app/`) — must match the app's registered redirect exactly. Used by the SPA and as the gateway's exchange `redirect_uri`. |
| `GATEWAY_VK_ID_CLIENT_SECRET` | secret | _(empty)_ | The VK ID "Web" app's protected key (`client_secret`) for the gateway's server-side confidential code exchange. A SEPARATE VK app from `GATEWAY_VK_APP_SECRET` (the Mini App). |
| `VITE_GATEWAY_URL` | variable | _(empty)_ | UI build-arg: gateway origin; empty = same-origin (the usual single-origin deploy). |
| `SMTP_RELAY_HOST` | variable | _(empty)_ | Selectel SMTP relay host for confirm-code email. Empty leaves the backend on the log mailer (email disabled) — the contour still boots. One relay for every contour (limit 100 msgs / 5 min). |
| `SMTP_RELAY_PORT` | variable | `465` | Relay port. No client certificate is needed (the server cert is validated against the system roots). |
@@ -212,11 +215,11 @@ players arrive.
`PROD_{POSTGRES_PASSWORD, GM_BASICAUTH_HASH, GRAFANA_ADMIN_PASSWORD, TELEGRAM_BOT_TOKEN,
TELEGRAM_PROMO_BOT_TOKEN, EXPORT_SIGN_KEY, REGISTRY_PASSWORD, SSH_KEY, SSH_KNOWN_HOSTS, BOTLINK_CA,
BOTLINK_GATEWAY_CERT, BOTLINK_GATEWAY_KEY, BOTLINK_BOT_CERT, BOTLINK_BOT_KEY,
SMTP_RELAY_USER, SMTP_RELAY_PASS}`; variables:
SMTP_RELAY_USER, SMTP_RELAY_PASS, GATEWAY_VK_ID_CLIENT_SECRET}`; variables:
`PROD_{REGISTRY_USER, MAIN_HOST, TG_HOST, CADDY_SITE_ADDRESS, GM_BASICAUTH_USER,
GRAFANA_ROOT_URL, LOG_LEVEL, DICT_VERSION, TELEGRAM_MINIAPP_URL, TELEGRAM_GAME_CHANNEL_ID,
TELEGRAM_CHAT_ID, TELEGRAM_BOT_USERNAME, VITE_TELEGRAM_BOT_ID, VITE_TELEGRAM_LINK,
VITE_TELEGRAM_GAME_CHANNEL_NAME, VITE_VK_APP_LINK,
VITE_TELEGRAM_GAME_CHANNEL_NAME, VITE_VK_APP_LINK, VITE_VK_APP_ID, VITE_VK_ID_REDIRECT_URL,
SMTP_RELAY_HOST, SMTP_RELAY_PORT, SMTP_RELAY_TLS, SMTP_RELAY_FROM, PUBLIC_BASE_URL,
SMTP_RELAY_ADMIN_FROM, ADMIN_EMAIL, SMTP_RELAY_SERVICE_FROM, SERVICE_EMAIL,
GRAFANA_SMTP_PORT, GF_SMTP_ENABLED}`.
+12
View File
@@ -190,6 +190,8 @@ services:
VITE_TELEGRAM_LINK: ${VITE_TELEGRAM_LINK:-}
VITE_TELEGRAM_GAME_CHANNEL_NAME: ${VITE_TELEGRAM_GAME_CHANNEL_NAME:-}
VITE_VK_APP_LINK: ${VITE_VK_APP_LINK:-}
VITE_VK_APP_ID: ${VITE_VK_APP_ID:-}
VITE_VK_ID_REDIRECT_URL: ${VITE_VK_ID_REDIRECT_URL:-}
VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-}
VITE_APP_VERSION: ${APP_VERSION:-dev}
# Go binary version (the SPA's VITE_APP_VERSION is the same git tag).
@@ -207,6 +209,14 @@ services:
# app's protected key (a pure offline HMAC, no VK API round-trip). Empty disables
# the VK auth path (auth.vk is then unregistered).
GATEWAY_VK_APP_SECRET: ${GATEWAY_VK_APP_SECRET:-}
# VK ID web login (browser VK-identity linking): the confidential OAuth 2.1 code
# exchange runs server-side under the VK ID "Web" app's protected key. This is a
# SEPARATE VK app from the Mini App above, so the credentials are distinct. The app id
# and redirect URL are the same values the SPA builds its authorize URL from (one
# source each). All three empty disables the link.vk.* ops.
GATEWAY_VK_ID_APP_ID: ${VITE_VK_APP_ID:-}
GATEWAY_VK_ID_CLIENT_SECRET: ${GATEWAY_VK_ID_CLIENT_SECRET:-}
GATEWAY_VK_ID_REDIRECT_URL: ${VITE_VK_ID_REDIRECT_URL:-}
# The reverse bot-link: the bot dials :9443 over mTLS; the backend admin relay
# reaches the gateway at :9092 (plaintext, internal). In the test contour both
# listeners stay on the internal network (the bot shares the VPN netns); in prod
@@ -264,6 +274,8 @@ services:
VITE_TELEGRAM_LINK: ${VITE_TELEGRAM_LINK:-}
VITE_TELEGRAM_GAME_CHANNEL_NAME: ${VITE_TELEGRAM_GAME_CHANNEL_NAME:-}
VITE_VK_APP_LINK: ${VITE_VK_APP_LINK:-}
VITE_VK_APP_ID: ${VITE_VK_APP_ID:-}
VITE_VK_ID_REDIRECT_URL: ${VITE_VK_ID_REDIRECT_URL:-}
VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-}
VITE_APP_VERSION: ${APP_VERSION:-dev}
restart: unless-stopped