feat(email): one-tap confirm deeplink + client language (PR1b) #162

Merged
developer merged 10 commits from feature/email-relay-pr1b into development 2026-07-03 06:58:12 +00:00
Owner

PR1b of the email feature: the one-tap confirm deeplink (deferred out of PR1a because it couples the email button, the SPA /confirm route and the backend endpoint) plus forwarding the client language on the email request. Builds on PR1a (#161).

Backend

  • Restore the deeplink token: migration 00006 adds email_confirmations.purpose + link_token_hash; each code carries an opaque 256-bit token (only its SHA-256 stored). ConfirmByToken resolves a token to a login (confirm + clear guest) or a link (attach when free, signal merge when owned elsewhere).
  • Endpoint POST /internal/sessions/email/confirm-link: login mints a session; a free link attaches + publishes a notify profile refresh; a taken address returns merge_required. Integration tests for all three branches.

Gateway

  • New confirmEmailLink edge method (auth.email.confirm_link): a new EmailConfirmLinkRequest/Result fbs table, transcode handler + encoder, backend-client call. It rides Execute under the existing service prefix — no proto or Caddyfile change.
  • Forward language on the email request (the backend already seeds it). Regenerated fbs bindings (Go + TS).
  • New notify profile sub-kind + notify.ProfileChanged.

UI

  • /confirm/ route + Confirm screen: a prefetch-safe button POSTs the token (a mail scanner's GET does not consume it). A login adopts the session and enters the app; a link shows confirmed / merge-in-the-app; an invalid/expired token asks for a new code. Exempt /confirm from the no-session /login redirect.
  • Forward app.locale on the email request; handle the profile live event by re-fetching the profile. i18n en+ru.

Notes

  • Migration 00006 is additive (ADD COLUMN, expand-contract) — goose forward-migrates on deploy, no contour wipe. No new deploy variables.
  • Test the deeplink via web once deployed: the login screen (email -> one-tap link -> magic-link) and a web guest binding email in the profile. TG/VK email-binding is PR2.

Verified locally

Go build/vet/gofmt + unit + integration (Email/Merge/ConfirmByToken); UI check/test:unit/build/e2e (176 passed). No proto/Caddyfile change.

PR1b of the email feature: the one-tap confirm deeplink (deferred out of PR1a because it couples the email button, the SPA /confirm route and the backend endpoint) plus forwarding the client language on the email request. Builds on PR1a (#161). ## Backend - Restore the deeplink token: migration 00006 adds email_confirmations.purpose + link_token_hash; each code carries an opaque 256-bit token (only its SHA-256 stored). ConfirmByToken resolves a token to a login (confirm + clear guest) or a link (attach when free, signal merge when owned elsewhere). - Endpoint POST /internal/sessions/email/confirm-link: login mints a session; a free link attaches + publishes a notify `profile` refresh; a taken address returns `merge_required`. Integration tests for all three branches. ## Gateway - New confirmEmailLink edge method (auth.email.confirm_link): a new EmailConfirmLinkRequest/Result fbs table, transcode handler + encoder, backend-client call. It rides Execute under the existing service prefix — no proto or Caddyfile change. - Forward `language` on the email request (the backend already seeds it). Regenerated fbs bindings (Go + TS). - New notify `profile` sub-kind + notify.ProfileChanged. ## UI - /confirm/<token> route + Confirm screen: a prefetch-safe button POSTs the token (a mail scanner's GET does not consume it). A login adopts the session and enters the app; a link shows confirmed / merge-in-the-app; an invalid/expired token asks for a new code. Exempt /confirm from the no-session /login redirect. - Forward app.locale on the email request; handle the `profile` live event by re-fetching the profile. i18n en+ru. ## Notes - Migration 00006 is additive (ADD COLUMN, expand-contract) — goose forward-migrates on deploy, no contour wipe. No new deploy variables. - Test the deeplink via web once deployed: the login screen (email -> one-tap link -> magic-link) and a web guest binding email in the profile. TG/VK email-binding is PR2. ## Verified locally Go build/vet/gofmt + unit + integration (Email/Merge/ConfirmByToken); UI check/test:unit/build/e2e (176 passed). No proto/Caddyfile change.
developer added 5 commits 2026-07-03 02:58:36 +00:00
Re-apply the deeplink backend deferred out of PR1a: migration 00006 adds
email_confirmations.purpose + link_token_hash (hand-edited jet), each issued code
now carries an opaque 256-bit token (only its SHA-256 stored), and ConfirmByToken
resolves a token to a login (confirm + clear guest) or a link (attach when free,
signal merge when owned elsewhere). issueCode now embeds the /app/#/confirm/<token>
link in the email. The confirm endpoint, gateway RPC and SPA route follow.
Add POST /internal/sessions/email/confirm-link: it verifies a deeplink token via
ConfirmByToken and, for a login, mints a session (the deeplink page signs in with
it); for a link, attaches the confirmed email and reports "confirmed" or
"merge_required" (the app drives the interactive merge). The token, not a request
session, is the authorization. Add LinkConfirmation.IsLogin() and integration tests
for the login, link and merge branches (the token is read from the mailed link).
The gateway RPC, live event and SPA route follow.
Add the confirm-link edge method (auth.email.confirm_link): a new
EmailConfirmLinkRequest/Result fbs table, the transcode const + handler + encoder,
and the backend-client call to the existing /sessions/email/confirm-link endpoint —
it rides Execute under the existing service prefix, so no proto/Caddy change. Add a
language field to EmailRequestRequest and forward it (the backend already seeds it).
Add the NotifyProfile sub-kind + notify.ProfileChanged, published by the confirm-link
handler on a successful link so an in-app session re-fetches its profile when the
email was confirmed in another browser. Regenerated fbs bindings (Go + TS).
Add the /confirm/<token> SPA route and Confirm screen: a prefetch-safe button
POSTs the token via a new confirmEmailLink RPC (client/transport/mock/codec +
ConfirmLinkResult model). A login adopts the minted session and enters the app; a
link shows confirmed / merge-in-the-app; an invalid or expired token asks for a new
code. Exempt /confirm from the no-session /login redirect. Forward the client
locale on the email request (authEmailRequest gains language → app.locale) so a
fresh web login email is localised. Handle the new 'profile' live-event sub-kind by
re-fetching the profile, so a link confirmed in another browser reflects in-app at
once. i18n en+ru.
docs+test(email): document the confirm deeplink + wire-contract tests
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 1m3s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m52s
65f2c87a74
Document the one-tap confirm deeplink in ARCHITECTURE (§4 the login magic-link /
link confirm+profile-refresh, prefetch-safe token) and the new notify 'profile'
sub-kind (§10), and add the one-tap link to the FUNCTIONAL email story (+ru). Add
codec round-trip assertions for the EmailRequest language field and
encodeEmailConfirmLink (the mock e2e bypasses the codec).
developer added 1 commit 2026-07-03 03:10:24 +00:00
fix(account): seed the email account display name from the local part
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 1m3s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m53s
5804f7266e
An email account was provisioned with no display name (unlike Telegram/VK, which
seed one), so an email login showed an empty name. Seed it from the email's local
part (before '@', trimmed and capped to the column width) on first contact; the
user can rename it later. Only new accounts are seeded — an existing account's name
is never overwritten.
developer added 2 commits 2026-07-03 03:18:25 +00:00
ConfirmByToken attached the confirmed email to the account but, on the link path,
skipped ClearGuest — so a guest who bound an email via the one-tap deeplink stayed a
guest (the code-based flow clears it in the link service). Clear the flag on a free
link too, promoting the guest to a durable account; the profile live event then
refreshes the open session. Integration test added.
feat(ui): auto-confirm the email deeplink on load
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 1m3s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m44s
1dca6741f1
Drop the confirm button: the /confirm screen confirms the token as soon as it
loads. 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. Update the ARCHITECTURE/FUNCTIONAL wording
accordingly and swap the confirm.prompt/action strings for confirm.busy (en+ru).
developer added 1 commit 2026-07-03 03:30:31 +00:00
feat(admin): search users by email + erase a bound email
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 1m3s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
356bf1a5ba
Add an exact (strict) email filter to the /users list (UserFilter.EmailExact →
a kind='email' identity match) with a search input, and an 'Erase email' action on
the user card that deletes the bound email identity and its pending confirmations,
freeing the address. It refuses to remove the account's only identity
(ErrLastIdentity), which would leave it unreachable. Integration tests for both.
developer added 1 commit 2026-07-03 03:54:30 +00:00
fix(ui): localise the confirm screen, drop the brand on the error state
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 1m3s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s
54af644429
The session-less /confirm page defaulted to English, so it showed the English
app title. Carry the recipient's language on the deeplink (?lang) and setLocale on
load so the page matches the email. Show a localised brand wordmark (Эрудит / Erudit,
matching the email) on the success state only; the invalid/expired state now shows
just the message, no header.
owner approved these changes 2026-07-03 06:57:32 +00:00
developer merged commit 76e7916ff6 into development 2026-07-03 06:58:12 +00:00
developer deleted branch feature/email-relay-pr1b 2026-07-03 06:58:13 +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#162