feat(account): provider linking, unlink & email change (PR2) #163
Reference in New Issue
Block a user
Delete Branch "feature/email-relay-pr2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
telegram/vk):POST /user/link/unlink→account.RemoveIdentity,which refuses removing the last identity (
ErrLastIdentity→ 409) so an accountnever becomes unreachable. The UI mirrors the guard (hides Unlink when one method
remains). Email is never unlinked — it is changed.
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.
the guest upgrade path (add email / add Telegram) is unchanged.
Surface
RemoveIdentity(kind),RequestChangeCode/ConfirmChange,replaceEmailIdentity,purposeChange(+ deeplink branch), branded ru/en change copy;routes
/user/link/unlink,/user/link/email/change/{request,confirm}.link.unlink,link.email.change.{request,confirm}ops (+ backendclient);new fbs
LinkUnlinkRequest(change reusesLinkEmail*). No new edge HTTP route → noCaddyfile change.
client/transport/mock/codec, ru/en i18n.
email/telegram_linked/vk_linked(first commits).Tests
encodeLinkUnlink+unlinked/changedstatuses. Template: change copy.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=changedeeplink.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.
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.The bare U+2709 rendered as a mono pseudo-glyph; add the U+FE0F variation selector so it shows as ✉️.