feat(account): provider linking, unlink & email change (PR2) #163

Merged
developer merged 8 commits from feature/email-relay-pr2 into development 2026-07-03 09:16:04 +00:00
Owner

PR2 — Provider linking, unlink, email change

Ships the profile sign-in-methods matrix on top of the live email (PR1a/PR1b):
add/change email, link Telegram on the web, unlink a linked provider. VK-on-web is
deferred (no VK OAuth); inside a Mini App the host provider is already linked.

Behaviour

  • Unlink (telegram/vk): POST /user/link/unlinkaccount.RemoveIdentity,
    which refuses removing the last identity (ErrLastIdentity → 409) so an account
    never becomes unreachable. The UI mirrors the guard (hides Unlink when one method
    remains). Email is never unlinked — it is changed.
  • Change email: mail a code (purpose=change) to the new address, then confirm
    (code or the one-tap deeplink) to atomically replace the account's email,
    freeing the old address. A new address owned by another account is refused
    without disclosure ("check the address or contact support") and never merged.
  • Add Telegram on the web via the login widget (existing link/merge machinery);
    the guest upgrade path (add email / add Telegram) is unchanged.

Surface

  • Backend: RemoveIdentity(kind), RequestChangeCode/ConfirmChange,
    replaceEmailIdentity, purposeChange (+ deeplink branch), branded ru/en change copy;
    routes /user/link/unlink, /user/link/email/change/{request,confirm}.
  • Gateway: link.unlink, link.email.change.{request,confirm} ops (+ backendclient);
    new fbs LinkUnlinkRequest (change reuses LinkEmail*). No new edge HTTP route → no
    Caddyfile change.
  • UI: profile matrix (email add/change, provider link/unlink, last-identity guard),
    client/transport/mock/codec, ru/en i18n.
  • Profile DTO already carries email/telegram_linked/vk_linked (first commits).

Tests

  • Integration: unlink last-identity guard; change-email replace / refuse-taken / deeplink.
  • Codec wire: encodeLinkUnlink + unlinked/changed statuses. Template: change copy.
  • e2e (Chromium + WebKit): change-email taken (non-disclosing) + free (replace);
    link→unlink Telegram.

Docs

  • FUNCTIONAL.md (+_ru): drop the stale "linking UI hidden" note; document the matrix.
  • ARCHITECTURE.md: unlink guard, non-disclosing atomic change, purpose=change deeplink.

Locally verified: Go build/vet/gofmt/unit, gateway, full integration package, codegen
idempotent, UI check/test/build, e2e (social/smoke/onboarding). Deletion (PR3) + Grafana/
admin alerts (PR4) follow.

## PR2 — Provider linking, unlink, email change Ships the profile **sign-in-methods matrix** on top of the live email (PR1a/PR1b): add/change email, link Telegram on the web, unlink a linked provider. VK-on-web is deferred (no VK OAuth); inside a Mini App the host provider is already linked. ### Behaviour - **Unlink** (`telegram`/`vk`): `POST /user/link/unlink` → `account.RemoveIdentity`, which **refuses removing the last identity** (`ErrLastIdentity` → 409) so an account never becomes unreachable. The UI mirrors the guard (hides Unlink when one method remains). **Email is never unlinked — it is changed.** - **Change email**: mail a code (`purpose=change`) to the new address, then confirm (code or the one-tap deeplink) to **atomically replace** the account's email, freeing the old address. A new address owned by **another** account is refused **without disclosure** ("check the address or contact support") and **never merged**. - **Add Telegram on the web** via the login widget (existing link/merge machinery); the guest upgrade path (add email / add Telegram) is unchanged. ### Surface - Backend: `RemoveIdentity(kind)`, `RequestChangeCode`/`ConfirmChange`, `replaceEmailIdentity`, `purposeChange` (+ deeplink branch), branded ru/en change copy; routes `/user/link/unlink`, `/user/link/email/change/{request,confirm}`. - Gateway: `link.unlink`, `link.email.change.{request,confirm}` ops (+ backendclient); new fbs `LinkUnlinkRequest` (change reuses `LinkEmail*`). No new edge HTTP route → no Caddyfile change. - UI: profile matrix (email add/change, provider link/unlink, last-identity guard), client/transport/mock/codec, ru/en i18n. - Profile DTO already carries `email`/`telegram_linked`/`vk_linked` (first commits). ### Tests - Integration: unlink last-identity guard; change-email replace / refuse-taken / deeplink. - Codec wire: `encodeLinkUnlink` + `unlinked`/`changed` statuses. Template: change copy. - e2e (Chromium + WebKit): change-email taken (non-disclosing) + free (replace); link→unlink Telegram. ### Docs - `FUNCTIONAL.md` (+`_ru`): drop the stale "linking UI hidden" note; document the matrix. - `ARCHITECTURE.md`: unlink guard, non-disclosing atomic change, `purpose=change` deeplink. Locally verified: Go build/vet/gofmt/unit, gateway, full integration package, codegen idempotent, UI check/test/build, e2e (social/smoke/onboarding). Deletion (PR3) + Grafana/ admin alerts (PR4) follow.
developer added 7 commits 2026-07-03 08:06:32 +00:00
Add email / telegram_linked / vk_linked to the Profile (fbs table + regenerated
Go/TS bindings, gateway ProfileResp + encodeProfile, backend DTO, UI model +
decode). They are filled outside the pure projection — Server.profileResponse now
reads the account's identities (like the banner seam) — and will drive the profile's
Add / Unlink / change-email controls.
Generalize the email-erase store op to any identity kind (with the same
last-identity guard and, for email, the pending-confirmation cleanup) so the
profile Unlink control can reuse it for Telegram/VK. RemoveEmailIdentity stays as a
thin wrapper for the admin console.
Unlink: POST /user/link/unlink (telegram|vk) via account.RemoveIdentity, refusing
the last identity; email is never unlinked. New fbs LinkUnlinkRequest + gateway
link.unlink op, returning the refreshed profile.

Change-email: purposeChange confirm-codes (RequestChangeCode/ConfirmChange) that
atomically replace the account's confirmed email (account.replaceEmailIdentity);
a new address owned by another account is refused without disclosure, never merged.
The one-tap deeplink handles purposeChange too. Reuses the LinkEmail* fbs tables;
gateway link.email.change.{request,confirm} ops + backendclient methods; branded
ru/en change-email copy.
Profile shows the account's sign-in methods for guests and durable accounts alike:
add or change email, link Telegram on the web (login widget), and unlink a linked
provider. Email is never unlinked (it is changed); unlink is offered only when
another identity remains, and a change to an address owned by another account shows
the non-disclosing 'check the address or contact support'. Add-VK-on-web stays
deferred (no VK OAuth).

Wires linkUnlink / changeEmailRequest / changeEmailConfirm through the client,
transport, mock and codec (encodeLinkUnlink + LinkResult 'unlinked'/'changed'
statuses); codec wire tests; ru/en i18n.
Integration: unlinking a provider keeps the other identities and refuses removing
the last one; change-email replaces the address (freeing the old), refuses a taken
address without merging, and works through the one-tap deeplink token. Unit: the
change-email template renders localised ru/en copy.
FUNCTIONAL (+ru): drop the stale 'linking UI hidden' note; document the profile
sign-in-methods matrix, the unlink last-identity guard, and the non-disclosing
atomic email change. ARCHITECTURE: unlink + change-email behaviour and the
purpose=change deeplink branch.
test(e2e): sign-in methods matrix — change-email + link/unlink Telegram
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 19s
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 1m37s
3f4792a39b
Replace the stale skipped linking specs: change-email refuses a taken address with
the non-disclosing message (never revealing the other account) and replaces a free
one; the web Telegram link control links then unlinks through the confirm dialog.
Chromium + WebKit.
developer added 1 commit 2026-07-03 09:05:37 +00:00
fix(profile): emoji-presentation envelope icon for the email row
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
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 1m58s
029aa2d4cc
The bare U+2709 rendered as a mono pseudo-glyph; add the U+FE0F variation selector
so it shows as ✉️.
owner approved these changes 2026-07-03 09:10:44 +00:00
developer merged commit 61c7da271c into development 2026-07-03 09:16:04 +00:00
developer deleted branch feature/email-relay-pr2 2026-07-03 09:16:04 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: developer/scrabble-game#163