Stage 11: account linking & merge (email + Telegram Login Widget)
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 11s
Tests · UI / test (push) Successful in 20s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 11s
Tests · UI / test (push) Successful in 20s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s
Link an email (confirm-code) or Telegram (web Login Widget) to the current account; if the identity already has its own account, merge the two into the one in use (the current account is primary, except a guest initiator whose durable counterpart wins). The merge runs in one transaction (internal/accountmerge): stats + hint wallet summed, paid_account ORed, identities/games/chat/complaints transferred, friends/blocks de-duplicated, the secondary kept as a merged_into tombstone so a shared finished game's no-cascade FKs hold; a shared active game blocks the merge. - migration 00009: accounts.paid_account, merged_into, merged_at (+ jetgen) - internal/link orchestrator; session.RevokeAllForAccount on merge - connector ValidateLoginWidget RPC + loginwidget HMAC validator - edge ops link.email.request/confirm/merge, link.telegram.confirm/merge; supersedes the Stage 8 email.bind.* surface (request never reveals 'taken' before the code is verified, so a probe cannot enumerate addresses) - UI Profile link section + irreversible-merge dialog; Telegram web sign-in - focused regression tests (merge core, guest inversion, active-game refusal, finished-shared-game kept), gateway transcode + connector + UI codec/e2e - docs: PLAN, ARCHITECTURE 3/4/9, FUNCTIONAL(+ru), module READMEs
This commit is contained in:
+33
-13
@@ -113,7 +113,9 @@ arrive from a platform rather than completing a mandatory registration).
|
||||
records live in `backend`, which stores only a **SHA-256 hash** of the opaque
|
||||
token (never the plaintext), keeps a warmed in-memory cache for fast
|
||||
resolution, and treats sessions as **revoke-only** — they have no TTL and live
|
||||
until explicitly revoked (`status` → `revoked`).
|
||||
until explicitly revoked (`status` → `revoked`). A revoke can target one token or,
|
||||
on an account merge (§4), **every** session of the retired account
|
||||
(`RevokeAllForAccount`, which also evicts them from the warm cache).
|
||||
- **Guest** = ephemeral web session (no platform, no email). A guest is backed by
|
||||
a durable `accounts` row flagged `is_guest` and carrying **no identity** — the
|
||||
row is a technical necessity (the `sessions` and `game_players` foreign keys
|
||||
@@ -134,17 +136,33 @@ arrive from a platform rather than completing a mandatory registration).
|
||||
authenticated account: a 6-digit code (stored only as a SHA-256 hash, 15-minute
|
||||
TTL, ≤ 5 attempts) is sent through a `Mailer` seam (an SMTP relay, or a
|
||||
development log mailer when none is configured) and, once verified, attaches a
|
||||
confirmed email identity. An email already confirmed by **another** account is
|
||||
refused — adopting it would be a merge, which Stage 11 owns. Accounts and
|
||||
identities use application-generated **UUIDv7** primary keys.
|
||||
- **Linking** is initiated from an authenticated profile: choose a platform →
|
||||
complete that platform's web-auth confirm → attach the identity to the
|
||||
current account.
|
||||
- **Merge**: if the identity being linked already has its own account with
|
||||
history, the two accounts are **merged into the current one (A is primary)**:
|
||||
statistics are summed, games and friends are transferred, duplicates are
|
||||
de-duplicated, the secondary account is retired. High blast-radius; an
|
||||
isolated, well-tested stage.
|
||||
confirmed email identity. Accounts and identities use application-generated
|
||||
**UUIDv7** primary keys. A service flag `paid_account` (lifetime one-time
|
||||
payment; no purchase flow yet) is carried on the account and ORed on a merge.
|
||||
- **Linking** (Stage 11) is initiated from an authenticated profile and proves
|
||||
control of the identity before attaching it: **email** through the confirm-code
|
||||
flow, **Telegram** through the web **Login Widget** (validated by the connector,
|
||||
HMAC under `SHA-256(bot_token)` — distinct from Mini App initData; the gateway
|
||||
passes the trusted `external_id` to the backend, as for `auth.telegram`). The
|
||||
request step **always** sends/accepts the proof (no pre-send "already taken"
|
||||
signal, so a probe cannot enumerate registered addresses); a required **merge**
|
||||
is revealed **only after** the proof is verified and is performed behind an
|
||||
explicit, irreversible confirmation. A free identity is simply attached (and a
|
||||
guest is promoted to durable, clearing `is_guest`).
|
||||
- **Merge** retires the account that owns the linked identity into the **current**
|
||||
account, in a single transaction (`internal/accountmerge`): statistics summed
|
||||
(max points kept), the hint wallet summed, `paid_account` ORed, identities
|
||||
repointed, games / chat / complaints transferred, friends and blocks
|
||||
de-duplicated (friendships keep the strongest status accepted>pending>declined),
|
||||
pending invitations/codes dropped, and the secondary kept as an **audit
|
||||
tombstone** (`accounts.merged_into`/`merged_at`) so a shared **finished** game's
|
||||
no-cascade foreign keys stay valid — its seat there is left untouched. A merge is
|
||||
**refused** only when the two share an **active** game. The current account is the
|
||||
primary, **except** when the initiator is a **guest** and the linked identity
|
||||
already has a **durable** owner: then the durable account wins, the guest's active
|
||||
games move into it, the guest is retired, and a **fresh session** is minted for the
|
||||
durable account (the client switches to it). The secondary's sessions are revoked
|
||||
(§3). High blast-radius; an isolated, well-tested stage.
|
||||
|
||||
## 5. Game engine integration (`scrabble-solver`)
|
||||
|
||||
@@ -337,7 +355,9 @@ requires (there is no DM surface; chat is per-game).
|
||||
- Tables: `accounts` (durable internal accounts; Stage 3 added the away-window
|
||||
columns `away_start`/`away_end` and the hint wallet `hint_balance`; Stage 6's
|
||||
migration `00005` added the `is_guest` flag for ephemeral guest rows; Stage 9's
|
||||
migration `00007` added the `notifications_in_app_only` out-of-app push toggle),
|
||||
migration `00007` added the `notifications_in_app_only` out-of-app push toggle;
|
||||
Stage 11's migration `00009` added the `paid_account` service flag and the
|
||||
merge-tombstone columns `merged_into`/`merged_at`),
|
||||
`identities` (platform/email/robot identities, unique `(kind, external_id)`;
|
||||
Stage 5's migration `00004` admits the `robot` kind),
|
||||
`sessions` (revoke-only opaque-token hashes), the Stage 3 game tables
|
||||
|
||||
+14
-10
@@ -36,11 +36,17 @@ out-of-app events (your turn, nudge, a found match, an invitation or friend requ
|
||||
arrive as a **Telegram notification** instead — unless the player keeps notifications
|
||||
in the app only (a profile setting, **on by default**).
|
||||
|
||||
### Accounts, linking & merge *(Stage 1 / 10)*
|
||||
First platform contact auto-provisions a durable account. From the profile a
|
||||
player links additional platform identities or an email via a confirm flow;
|
||||
linking an identity that already has history merges it into the current
|
||||
account (stats summed, games/friends transferred).
|
||||
### Accounts, linking & merge *(Stage 1 / 11)*
|
||||
First platform contact auto-provisions a durable account. From the profile a player
|
||||
links an email (via a confirm code) or their Telegram (via the web sign-in); a guest
|
||||
who links their first identity becomes a durable account. The "already taken" status
|
||||
of an identity is never revealed before the code/sign-in is verified. If the linked
|
||||
identity already belongs to another account, the player is shown an explicit,
|
||||
**irreversible** confirmation and the two accounts are merged into the one they are
|
||||
using (statistics summed, games and friends transferred, duplicates removed) — except
|
||||
when a guest links an identity that already has a durable account, where the durable
|
||||
account is kept and the guest's games move into it. A merge is blocked only while the
|
||||
two accounts share a game still in progress.
|
||||
|
||||
### Lobby & matchmaking *(Stage 4)*
|
||||
Bottom tab menu: **my games**, **profile**. Auto-match (always 2 players) joins a
|
||||
@@ -95,11 +101,9 @@ nudge is part of the game chat); the out-of-app push is delivered via the platfo
|
||||
### Profile & settings *(Stage 4 / 8)*
|
||||
Edit the display name (letters joined by single space / "." / "_" separators, up to
|
||||
32 characters), the timezone (chosen as a UTC offset), the daily away window (on a
|
||||
10-minute grid, at most 12 hours, wrapping midnight) and the block toggles, and bind
|
||||
an email by confirm-code: the backend emails a short code that,
|
||||
once entered, attaches the email to the account (an email already confirmed by
|
||||
another account cannot be taken — that is a merge, a later stage). Linked platform
|
||||
accounts and merge arrive in Stage 11.
|
||||
10-minute grid, at most 12 hours, wrapping midnight) and the block toggles. Linking
|
||||
an email or Telegram and merging accounts are covered under "Accounts, linking &
|
||||
merge" (Stage 11).
|
||||
|
||||
### History & statistics *(Stage 3 / 8)*
|
||||
Finished games are archived in a dictionary-independent form and exportable to
|
||||
|
||||
+12
-9
@@ -37,11 +37,17 @@ Mini App** авторизует по подписанным `initData` плат
|
||||
вместо этого **уведомлением в Telegram** — если только игрок не оставил уведомления
|
||||
только в приложении (настройка профиля, **включена по умолчанию**).
|
||||
|
||||
### Аккаунты, привязка и слияние *(Stage 1 / 10)*
|
||||
### Аккаунты, привязка и слияние *(Stage 1 / 11)*
|
||||
Первый контакт с платформы заводит постоянный аккаунт. Из профиля игрок
|
||||
привязывает другие платформенные личности или email через confirm-поток;
|
||||
привязка личности, у которой уже есть история, сливает её в текущий аккаунт
|
||||
(статистика суммируется, игры/друзья переносятся).
|
||||
привязывает email (по confirm-коду) или свой Telegram (через веб-вход); гость,
|
||||
привязавший первую личность, становится постоянным аккаунтом. Факт «личность уже
|
||||
занята» не раскрывается до проверки кода/входа. Если привязываемая личность уже
|
||||
принадлежит другому аккаунту, игроку показывают явное **необратимое**
|
||||
подтверждение, и два аккаунта сливаются в тот, под которым он сейчас работает
|
||||
(статистика суммируется, игры и друзья переносятся, дубликаты убираются), — кроме
|
||||
случая, когда гость привязывает личность с уже существующим постоянным аккаунтом:
|
||||
тогда сохраняется постоянный аккаунт, а игры гостя переходят в него. Слияние
|
||||
запрещено, только пока у аккаунтов есть общая незавершённая игра.
|
||||
|
||||
### Лобби и подбор *(Stage 4)*
|
||||
Нижнее tab-меню: **мои игры**, **профиль**. Авто-подбор (всегда 2 игрока)
|
||||
@@ -98,11 +104,8 @@ push доставляется через платформу.
|
||||
Редактирование отображаемого имени (буквы, разделённые одиночными пробелом / «.» /
|
||||
«_», до 32 символов), таймзоны (выбор смещения от UTC), суточного окна отсутствия
|
||||
(away; сетка по 10 минут, не более 12 часов, с переходом через полночь) и
|
||||
переключателей блокировок, а также привязка email по confirm-коду: backend шлёт на
|
||||
почту короткий код, и после ввода email
|
||||
привязывается к аккаунту (email, уже подтверждённый другим аккаунтом, занять
|
||||
нельзя — это слияние, отдельный этап). Привязанные платформенные аккаунты и
|
||||
слияние появятся в Stage 11.
|
||||
переключателей блокировок. Привязка email и Telegram, а также слияние аккаунтов
|
||||
вынесены в раздел «Аккаунты, привязка и слияние» (Stage 11).
|
||||
|
||||
### История и статистика *(Stage 3 / 8)*
|
||||
Завершённые партии архивируются в независимом от словаря виде и экспортируются
|
||||
|
||||
Reference in New Issue
Block a user