test+docs(email): squat-fix coverage and the email-pipeline docs
Add an integration test asserting the guest-until-confirmed squat fix (an email-login account is a reapable guest until the code is confirmed, then durable). Document the branded relay pipeline in ARCHITECTURE (go-mail, TLS by port, no client cert, PUBLIC_BASE_URL anti-injection, per-recipient send throttle, guest-until-confirmed), FUNCTIONAL (+ru), TESTING and the backend README config table (BACKEND_SMTP_* + BACKEND_PUBLIC_BASE_URL).
This commit is contained in:
+6
-5
@@ -214,11 +214,12 @@ internal/banview/ # gateway active-ban mirror: the console's Active IP bans p
|
|||||||
| `BACKEND_LOBBY_ROBOT_WAIT` | `10s` | Auto-match wait before a robot is substituted for a missing human. |
|
| `BACKEND_LOBBY_ROBOT_WAIT` | `10s` | Auto-match wait before a robot is substituted for a missing human. |
|
||||||
| `BACKEND_LOBBY_REAPER_INTERVAL` | `1s` | How often the substitution reaper scans for over-waited players. |
|
| `BACKEND_LOBBY_REAPER_INTERVAL` | `1s` | How often the substitution reaper scans for over-waited players. |
|
||||||
| `BACKEND_ROBOT_DRIVE_INTERVAL` | `30s` | How often the robot driver scans for due robot turns. |
|
| `BACKEND_ROBOT_DRIVE_INTERVAL` | `30s` | How often the robot driver scans for due robot turns. |
|
||||||
| `BACKEND_SMTP_HOST` | — | Email relay host. **Empty selects the development log mailer** (the confirm-code is logged, not sent). |
|
| `BACKEND_SMTP_HOST` | — | Confirm-code relay host. **Empty selects the development log mailer** (the code is logged, not sent). |
|
||||||
| `BACKEND_SMTP_PORT` | `587` | Email relay port. |
|
| `BACKEND_SMTP_PORT` | `587` | Relay port. `465` selects implicit TLS; any other port uses STARTTLS. No client certificate is needed (the server cert is validated against the system roots). |
|
||||||
| `BACKEND_SMTP_USERNAME` | — | SMTP user; empty relays without authentication. |
|
| `BACKEND_SMTP_USERNAME` | — | SMTP AUTH user; empty relays without authentication. |
|
||||||
| `BACKEND_SMTP_PASSWORD` | — | SMTP password. |
|
| `BACKEND_SMTP_PASSWORD` | — | SMTP AUTH password. |
|
||||||
| `BACKEND_SMTP_FROM` | `no-reply@localhost` | Envelope/From address for confirm-codes. |
|
| `BACKEND_SMTP_FROM` | `no-reply@localhost` | From address. A deployed contour must use the prod domain (the relay only accepts its verified sender domain). |
|
||||||
|
| `BACKEND_PUBLIC_BASE_URL` | — | Canonical public origin (scheme + host) for links in the email. **Required when `BACKEND_SMTP_HOST` is set.** Never derived from a request Host header (anti-injection). |
|
||||||
| `BACKEND_CONNECTOR_ADDR` | — | the gateway bot-link relay gRPC address for admin-console operator broadcasts. Empty disables broadcasts. |
|
| `BACKEND_CONNECTOR_ADDR` | — | the gateway bot-link relay gRPC address for admin-console operator broadcasts. Empty disables broadcasts. |
|
||||||
| `BACKEND_GUEST_REAP_INTERVAL` | `1h` | How often the abandoned-guest reaper sweeps. |
|
| `BACKEND_GUEST_REAP_INTERVAL` | `1h` | How often the abandoned-guest reaper sweeps. |
|
||||||
| `BACKEND_GUEST_RETENTION` | `720h` | Account age past which a guest with no game seat is deleted. |
|
| `BACKEND_GUEST_RETENTION` | `720h` | Account age past which a guest with no game seat is deleted. |
|
||||||
|
|||||||
@@ -244,3 +244,37 @@ func TestEmailLoginFlow(t *testing.T) {
|
|||||||
t.Errorf("returning login account = %s, want %s", acc2.ID, accountID)
|
t.Errorf("returning login account = %s, want %s", acc2.ID, accountID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestEmailLoginProvisionsGuestUntilConfirmed covers the squat fix: an email-login
|
||||||
|
// account is a guest (reapable, so an abandoned never-confirmed login frees its
|
||||||
|
// address) until the code is confirmed, which promotes it to a durable account.
|
||||||
|
func TestEmailLoginProvisionsGuestUntilConfirmed(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
store := account.NewStore(testDB)
|
||||||
|
mailer := &capturingMailer{}
|
||||||
|
svc := account.NewEmailService(store, mailer, "https://erudit-game.ru")
|
||||||
|
email := "squat-" + uuid.NewString() + "@example.com"
|
||||||
|
|
||||||
|
id, err := svc.RequestLoginCode(ctx, email, "", "en")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request login code: %v", err)
|
||||||
|
}
|
||||||
|
before, err := store.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get before confirm: %v", err)
|
||||||
|
}
|
||||||
|
if !before.IsGuest {
|
||||||
|
t.Error("an unconfirmed email-login account must be a guest so it is reapable")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := svc.LoginWithCode(ctx, email, sixDigit.FindString(mailer.lastBody)); err != nil {
|
||||||
|
t.Fatalf("login: %v", err)
|
||||||
|
}
|
||||||
|
after, err := store.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get after confirm: %v", err)
|
||||||
|
}
|
||||||
|
if after.IsGuest {
|
||||||
|
t.Error("confirming the login must clear the guest flag (promote to durable)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+11
-3
@@ -232,9 +232,17 @@ arrive from a platform rather than completing a mandatory registration).
|
|||||||
`confirmed` flag. A synthetic `kind='robot'` identity backs each pooled
|
`confirmed` flag. A synthetic `kind='robot'` identity backs each pooled
|
||||||
robot opponent (§7). The **email confirm-code flow** binds an email to the
|
robot opponent (§7). The **email confirm-code flow** binds an email to the
|
||||||
authenticated account: a 6-digit code (stored only as a SHA-256 hash, 15-minute
|
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
|
TTL, ≤ 5 attempts) is sent as a **branded HTML + plain-text email** through a
|
||||||
development log mailer when none is configured) and, once verified, attaches a
|
`Mailer` seam (go-mail over the shared relay — implicit TLS on port 465 or
|
||||||
confirmed email identity. Accounts and identities use application-generated
|
STARTTLS, the server certificate validated against the system roots, no client
|
||||||
|
certificate; a development log mailer when no relay is configured) and, once
|
||||||
|
verified, attaches a confirmed email identity. Sends are throttled per recipient
|
||||||
|
(a 60-second cooldown and a five-per-hour cap). Links in the email are built from
|
||||||
|
a configured public base URL, never a request Host header (anti-injection). An
|
||||||
|
**email-login** account is created flagged `is_guest` and stays reapable until the
|
||||||
|
code is confirmed — so an abandoned, never-confirmed login frees its reserved
|
||||||
|
address — with confirming clearing the flag. Accounts and identities use
|
||||||
|
application-generated
|
||||||
**UUIDv7** primary keys. A service flag `paid_account` (lifetime one-time
|
**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.
|
payment; no purchase flow yet) is carried on the account and ORed on a merge.
|
||||||
- **Linking** is initiated from an authenticated profile and proves
|
- **Linking** is initiated from an authenticated profile and proves
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ A **VK Mini App** launch works the same way: it authenticates from VK's signed l
|
|||||||
the name in the signed launch). While the theme preference is "auto" the app follows the VK
|
the name in the signed launch). While the theme preference is "auto" the app follows the VK
|
||||||
client's light/dark scheme, and the layout clears the VK mobile home bar. The same quiet-retry
|
client's light/dark scheme, and the layout clears the VK mobile home bar. The same quiet-retry
|
||||||
"couldn't load" screen applies inside VK.
|
"couldn't load" screen applies inside VK.
|
||||||
|
**Email** sign-in (web) mails a branded six-digit confirmation code — localized
|
||||||
|
(en/ru), no images — to the address; entering it signs the player in. A new address
|
||||||
|
is provisioned on the first request but only becomes a durable account once the code
|
||||||
|
is confirmed, so an abandoned, never-confirmed attempt is cleaned up and its address
|
||||||
|
freed. Sends are rate-limited (a short cooldown between codes and a small hourly cap
|
||||||
|
per address), so a mistyped address or an impatient tap cannot flood an inbox.
|
||||||
Telegram runs a **single bot**: every player uses
|
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
|
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
|
player's own **interface language** (en/ru). A separate optional **promo bot** can run alongside the
|
||||||
|
|||||||
@@ -65,7 +65,14 @@ launch-параметрам VK (их проверяет gateway), и при пе
|
|||||||
так как VK не кладёт имя в подписанный запуск). Пока тема в режиме «авто», приложение следует
|
так как VK не кладёт имя в подписанный запуск). Пока тема в режиме «авто», приложение следует
|
||||||
светлой/тёмной схеме VK-клиента, а раскладка обходит нижнюю home-bar VK на мобильных. Тот же экран
|
светлой/тёмной схеме VK-клиента, а раскладка обходит нижнюю home-bar VK на мобильных. Тот же экран
|
||||||
тихого повтора «не удалось
|
тихого повтора «не удалось
|
||||||
загрузить» действует и внутри VK. Telegram держит **единого бота**: все игроки пользуются одним
|
загрузить» действует и внутри VK.
|
||||||
|
**Email**-вход (в вебе) отправляет на адрес брендированный шестизначный код подтверждения —
|
||||||
|
локализованный (ru/en), без картинок; после ввода игрок входит. Новый адрес заводится при первом
|
||||||
|
запросе, но становится постоянным аккаунтом только после подтверждения кода — так брошенная,
|
||||||
|
неподтверждённая попытка вычищается, а адрес освобождается. Отправки ограничены по частоте (короткая
|
||||||
|
пауза между кодами и небольшой часовой лимит на адрес), чтобы опечатка в адресе или нетерпеливый тап
|
||||||
|
не завалили почтовый ящик.
|
||||||
|
Telegram держит **единого бота**: все игроки пользуются одним
|
||||||
и тем же ботом, а весь его чат и внеприложенческие уведомления пишутся на **языке
|
и тем же ботом, а весь его чат и внеприложенческие уведомления пишутся на **языке
|
||||||
интерфейса** самого игрока (en/ru). Рядом с основным может работать отдельный опциональный
|
интерфейса** самого игрока (en/ru). Рядом с основным может работать отдельный опциональный
|
||||||
**промо-бот** — его единственная задача отвечать на `/start` коротким сообщением и кнопкой,
|
**промо-бот** — его единственная задача отвечать на `/start` коротким сообщением и кнопкой,
|
||||||
|
|||||||
+4
-1
@@ -69,7 +69,10 @@ tests or touching CI.
|
|||||||
content and block-visibility rules, the nudge turn/rate-limit rules, the
|
content and block-visibility rules, the nudge turn/rate-limit rules, the
|
||||||
invitation flow (all-accept starts the game, decline cancels, lazy expiry,
|
invitation flow (all-accept starts the game, decline cancels, lazy expiry,
|
||||||
inviter-only cancel), and the email confirm-code flow (request/confirm, taken
|
inviter-only cancel), and the email confirm-code flow (request/confirm, taken
|
||||||
email, expiry and attempt-cap) with a fixture mailer. It also covers the
|
email, expiry and attempt-cap, and the guest-until-confirmed login) with a fixture
|
||||||
|
mailer. The branded email template render (en/ru subject, code and body) and the
|
||||||
|
per-recipient send-rate limiter (cooldown + hourly cap) are pure account-package
|
||||||
|
unit tests, needing no database. It also covers the
|
||||||
**befriend-an-opponent** gate (a request needs a shared game), the **permanent
|
**befriend-an-opponent** gate (a request needs a shared game), the **permanent
|
||||||
decline** and 30-day re-send rule, the **one-time friend code** (issue/redeem,
|
decline** and 30-day re-send rule, the **one-time friend code** (issue/redeem,
|
||||||
self/single-use, decline-bypass), `ListInvitations`, the zero-value `GetStats`, and
|
self/single-use, decline-bypass), `ListInvitations`, the zero-value `GetStats`, and
|
||||||
|
|||||||
Reference in New Issue
Block a user