feat(email): PWA login sends code only; drop admin link from alert emails
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s

Two email changes, per the owner:

1. Code-only login from an installed PWA. A login requested while running as a
   standalone PWA now omits the one-tap confirm link from the email — the link
   would open in a separate browser whose minted session cannot reach the PWA,
   stranding the login. The code is typed in the same window instead. The client
   sends a `pwa` flag (isStandalone) on the email-code request (a new FBS field,
   threaded through the gateway); the backend omits the deeplink when it is set,
   reusing the existing deletion-code link-omission path. Non-PWA browser logins
   keep the one-tap link. No polling, no migration.

2. Security: the operator alert digest no longer embeds the admin-console (/_gm)
   URL. An admin link must never travel in an email, where a mail provider could
   cache or index it; the operator opens the console directly.

Tests: an inttest asserting the PWA login email omits the link (and a browser one
keeps it); a codec round-trip of the new pwa field; the alert-digest test flipped
to guard the admin link is absent. Docs: ARCHITECTURE + FUNCTIONAL (+ru).
This commit is contained in:
Ilia Denisov
2026-07-05 22:13:21 +02:00
parent d19609f87d
commit 061366da5a
21 changed files with 139 additions and 57 deletions
+6 -2
View File
@@ -244,7 +244,9 @@ arrive from a platform rather than completing a mandatory registration).
256-bit token stored only as its SHA-256, 12-hour TTL): a login mints a session in the
browser that opens it (magic-link), a link confirms the identity and emits a `notify`
profile-refresh to the in-app session, a change switches the account's email in place,
and a would-be merge is deferred to the interactive flow. The confirm runs on load; the token rides the URL fragment (never
and a would-be merge is deferred to the interactive flow. A login requested from an installed **PWA** omits the deeplink
entirely — its email carries the code only, typed in the same window, since the link would open
in a separate browser whose minted session cannot reach the PWA. The confirm runs on load; the token rides the URL fragment (never
sent to the server), so a plain link prefetch cannot reach it, and the manual
six-digit code is the fallback if an aggressive scanner runs the page. An
**email-login** account is created flagged `is_guest` and stays reapable until the
@@ -1041,7 +1043,9 @@ link — misses the event; while an add-email confirmation is pending the client
(`internal/adminalert`, started from `main`) emails the operator (`ADMIN_EMAIL`, from a
distinct `SMTP_RELAY_ADMIN_FROM`) when new player feedback or word complaints arrive,
**coalescing** a burst into one digest per interval; both recipients may be several
comma-separated addresses. Both paths are inert unless configured.
comma-separated addresses. The digest carries **no admin-console link** — an admin URL must
never travel in an email, where a mail provider could cache or index it; the operator opens the
console directly. Both paths are inert unless configured.
- Per-request server-side timing via gin middleware from day one (the access log
carries method, route, status, latency and the active trace id). A
client-measured RTT piggybacked on the next request is a later enhancement.
+3 -1
View File
@@ -76,7 +76,9 @@ per address), so a mistyped address or an impatient tap cannot flood an inbox. T
also carries a **one-tap link** that confirms the address — or signs the player straight
in — in a single tap; opening it in another browser reflects in the app at once, and a
mail scanner that merely fetches the link cannot reach it — the token rides the URL
fragment, which is never sent to the server.
fragment, which is never sent to the server. A sign-in requested from an **installed PWA** omits
this link: the email carries only the code, entered in the same window, since the link would open
in a separate browser the app cannot reach.
Telegram runs a **single bot**: every player uses
the same bot, and all of its chat and out-of-app notifications are written in the
player's own **interface language** (en/ru). A separate optional **promo bot** can run alongside the
+3 -1
View File
@@ -80,7 +80,9 @@ launch-параметрам VK (их проверяет gateway), и при пе
не завалили почтовый ящик. В письме также есть **ссылка одного нажатия**, которая подтверждает
адрес — или сразу выполняет вход — в один тап; открытие её в другом браузере тут же отражается в
приложении, а почтовый сканер, который просто загружает ссылку, до токена не дотянется —
он едет в URL-фрагменте, который не уходит на сервер.
он едет в URL-фрагменте, который не уходит на сервер. Вход, запрошенный из **установленного
PWA**, эту ссылку не содержит: в письме только код, который вводится в том же окне (ссылка
открылась бы в отдельном браузере, недоступном приложению).
Telegram держит **единого бота**: все игроки пользуются одним
и тем же ботом, а весь его чат и внеприложенческие уведомления пишутся на **языке
интерфейса** самого игрока (en/ru). Рядом с основным может работать отдельный опциональный