From 0a0a9e5a8d4ce279f172d4d45178546e1e0f2168 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 10 Jul 2026 11:59:20 +0200 Subject: [PATCH] fix(deploy): wire the Robokassa direct rail into the prod deploy + rollback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Robokassa credentials reached only the test contour (ci.yaml → TEST_ secrets). The prod-deploy / prod-rollback workflows and write-prod-env.sh never rendered BACKEND_ROBOKASSA_*, so on prod the shop login was empty and the direct RUB rail stayed disabled — the PROD_BACKEND_ROBOKASSA_* secrets went nowhere. Export the shop login + Password1/Password2 (secrets) and a BACKEND_ROBOKASSA_TEST flag (a variable, so go-live is a flag flip not a secret rotation) from both prod workflows, and emit the four ROBOKASSA_* vars from write-prod-env.sh (the shared deploy/rollback env renderer) so a rollback keeps the rail up. The compose already maps ROBOKASSA_* → BACKEND_ROBOKASSA_*. Document the new secrets/variable in the deploy README and .env.example. Password3 (Robokassa's JWT-invoice API) is unused. --- .gitea/workflows/prod-deploy.yaml | 8 ++++++++ .gitea/workflows/prod-rollback.yaml | 6 ++++++ deploy/.env.example | 11 +++++++++++ deploy/README.md | 3 +++ deploy/write-prod-env.sh | 4 ++++ 5 files changed, 32 insertions(+) diff --git a/.gitea/workflows/prod-deploy.yaml b/.gitea/workflows/prod-deploy.yaml index 874f8c1..a9038de 100644 --- a/.gitea/workflows/prod-deploy.yaml +++ b/.gitea/workflows/prod-deploy.yaml @@ -99,6 +99,14 @@ jobs: GRAFANA_ADMIN_PASSWORD: ${{ secrets.PROD_GRAFANA_ADMIN_PASSWORD }} TELEGRAM_BOT_TOKEN: ${{ secrets.PROD_TELEGRAM_BOT_TOKEN }} GATEWAY_VK_APP_SECRET: ${{ secrets.GATEWAY_VK_APP_SECRET }} + # Robokassa direct-rail (backend BACKEND_ROBOKASSA_*): the prod shop login + the pass phrases + # that sign the launch request / verify the Result callback, and the test-mode flag — a var so + # go-live is a flag flip, not a secret redeploy ("1" runs test payments against the test + # passwords; empty/"0" is live). An empty login leaves the direct rail disabled. + ROBOKASSA_MERCHANT_LOGIN: ${{ secrets.PROD_BACKEND_ROBOKASSA_MERCHANT_LOGIN }} + ROBOKASSA_PASSWORD1: ${{ secrets.PROD_BACKEND_ROBOKASSA_PASSWORD1 }} + ROBOKASSA_PASSWORD2: ${{ secrets.PROD_BACKEND_ROBOKASSA_PASSWORD2 }} + ROBOKASSA_TEST: ${{ vars.PROD_BACKEND_ROBOKASSA_TEST }} # VK ID web login: the "Web" app id (the gateway reuses it as GATEWAY_VK_ID_APP_ID at # runtime) + the app's protected key. Both shared across contours. The redirect URL is # derived from PUBLIC_BASE_URL in deploy/write-prod-env.sh. diff --git a/.gitea/workflows/prod-rollback.yaml b/.gitea/workflows/prod-rollback.yaml index 19ca854..c461093 100644 --- a/.gitea/workflows/prod-rollback.yaml +++ b/.gitea/workflows/prod-rollback.yaml @@ -61,6 +61,12 @@ jobs: # the SAME env.sh (email / VK login / Grafana alerts survive a rollback). TELEGRAM_MINIAPP_URL # and GRAFANA_ROOT_URL are derived from PUBLIC_BASE_URL in deploy/write-prod-env.sh. GATEWAY_VK_APP_SECRET: ${{ secrets.GATEWAY_VK_APP_SECRET }} + # Robokassa direct-rail: the rollback re-renders the same runtime env (write-prod-env.sh), so + # it must carry the same credentials or the direct rail goes dark after a rollback. + ROBOKASSA_MERCHANT_LOGIN: ${{ secrets.PROD_BACKEND_ROBOKASSA_MERCHANT_LOGIN }} + ROBOKASSA_PASSWORD1: ${{ secrets.PROD_BACKEND_ROBOKASSA_PASSWORD1 }} + ROBOKASSA_PASSWORD2: ${{ secrets.PROD_BACKEND_ROBOKASSA_PASSWORD2 }} + ROBOKASSA_TEST: ${{ vars.PROD_BACKEND_ROBOKASSA_TEST }} VITE_VK_APP_ID: ${{ vars.VITE_VK_APP_ID }} GATEWAY_VK_ID_CLIENT_SECRET: ${{ secrets.GATEWAY_VK_ID_CLIENT_SECRET }} GATEWAY_HONEYTOKEN: ${{ secrets.PROD_GATEWAY_HONEYTOKEN }} diff --git a/deploy/.env.example b/deploy/.env.example index 96ccd77..8c624af 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -125,6 +125,17 @@ GATEWAY_VK_APP_SECRET= # come from VITE_VK_APP_ID / VITE_VK_ID_REDIRECT_URL above. All three empty disables link.vk.*. GATEWAY_VK_ID_CLIENT_SECRET= +# --- Payments: Robokassa (direct RUB rail) ---------------------------------- +# The shop's merchant login + the two pass phrases (Password1 signs the launch request, +# Password2 signs/verifies the Result callback). An empty login leaves the direct rail off. +# ROBOKASSA_TEST=1 runs test payments against the shop's TEST pass phrases (no real money); +# empty/0 is live. Mapped in compose to BACKEND_ROBOKASSA_*; Gitea TEST_/PROD_ secrets, with +# PROD_BACKEND_ROBOKASSA_TEST a variable so go-live is a flag flip, not a secret redeploy. +ROBOKASSA_MERCHANT_LOGIN= +ROBOKASSA_PASSWORD1= +ROBOKASSA_PASSWORD2= +ROBOKASSA_TEST= + # --- Gateway anti-abuse ------------------------------------------------------ # Planted honeytoken bearer value: any request presenting it is flagged — a 24h IP ban # where the IP ban is on (prod), logs + a ban metric otherwise (test). Plant the value diff --git a/deploy/README.md b/deploy/README.md index c9d7170..a10ece9 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -76,6 +76,9 @@ compose binds from this directory. | `GM_BASICAUTH_HASH` | secret | bcrypt hash gating `/_gm` (admin console + Grafana). Generate with `docker run --rm caddy:2-alpine caddy hash-password --plaintext ''`. | | `TELEGRAM_MINIAPP_URL` | derived | The Mini App URL the bot hands out in deep links / buttons. The deploy derives `PUBLIC_BASE_URL + /telegram/`; set it directly only for a local run (compose still `:?`-requires it). | | `EXPORT_SIGN_KEY` | secret | HMAC key signing the public finished-game export download URLs (`/dl/*`). Generate with `openssl rand -base64 32`. | +| `BACKEND_ROBOKASSA_MERCHANT_LOGIN` | secret | Robokassa shop login for the direct RUB rail. Empty leaves the rail off. | +| `BACKEND_ROBOKASSA_PASSWORD1` / `…_PASSWORD2` | secret | Robokassa pass phrases: Password1 signs the launch request, Password2 signs/verifies the Result callback. Use the shop's **test** pair while `…_ROBOKASSA_TEST=1`, the **live** pair for real money. (Password3 — Robokassa's JWT-invoice API — is unused.) | +| `BACKEND_ROBOKASSA_TEST` | **variable** | `1` runs test payments against the test pass phrases (no real money); empty/`0` is live. A variable, not a secret, so go-live is a flag flip + redeploy, not a secret rotation. | **Plus the bot token** — `TELEGRAM_BOT_TOKEN` (secret), shared by the validator (HMAC secret) and the bot (Bot API). It defaults to empty in compose, but both **fail at diff --git a/deploy/write-prod-env.sh b/deploy/write-prod-env.sh index 6899efb..cd66903 100755 --- a/deploy/write-prod-env.sh +++ b/deploy/write-prod-env.sh @@ -52,6 +52,10 @@ export APP_VERSION='$APP_VERSION' export TELEGRAM_BOT_TOKEN='$TELEGRAM_BOT_TOKEN' export TELEGRAM_MINIAPP_URL='$TELEGRAM_MINIAPP_URL' export GATEWAY_VK_APP_SECRET='$GATEWAY_VK_APP_SECRET' +export ROBOKASSA_MERCHANT_LOGIN='$ROBOKASSA_MERCHANT_LOGIN' +export ROBOKASSA_PASSWORD1='$ROBOKASSA_PASSWORD1' +export ROBOKASSA_PASSWORD2='$ROBOKASSA_PASSWORD2' +export ROBOKASSA_TEST='$ROBOKASSA_TEST' export VITE_VK_APP_ID='$VITE_VK_APP_ID' export VITE_VK_ID_REDIRECT_URL='$VITE_VK_ID_REDIRECT_URL' export GATEWAY_VK_ID_CLIENT_SECRET='$GATEWAY_VK_ID_CLIENT_SECRET'