feat(account): brand and harden the email pipeline
Swap the net/smtp mailer for go-mail behind the existing Mailer seam: the Message struct now carries a text + HTML body, TLS mode is chosen from the port (implicit TLS on 465, else mandatory STARTTLS), a dial timeout bounds the synchronous send, and no client certificate is needed. Add a branded, image-free, mobile-friendly ru/en HTML template (with a plain-text alternative) rendering a large readable code and an ignore-notice footer with a landing link. Add BACKEND_PUBLIC_BASE_URL config (the canonical origin for the email footer link, never the request Host — anti-injection), required when a relay is configured. Fix the email-login address squat: ProvisionEmail creates the account flagged is_guest until the code is confirmed, so an abandoned login is reaped like any guest and its address freed; confirming (login or link) clears the flag. Seed the new account's language from the client, plumbed through the email-login request. The confirm deeplink, its transport surface and the send rate-limit land in follow-up work.
This commit is contained in:
@@ -19,8 +19,8 @@ import (
|
||||
// recover the confirm-code from the body.
|
||||
type capturingMailer struct{ lastBody string }
|
||||
|
||||
func (m *capturingMailer) Send(_ context.Context, _, _, body string) error {
|
||||
m.lastBody = body
|
||||
func (m *capturingMailer) Send(_ context.Context, msg account.Message) error {
|
||||
m.lastBody = msg.Text
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestEmailConfirmFlow(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := account.NewStore(testDB)
|
||||
mailer := &capturingMailer{}
|
||||
svc := account.NewEmailService(store, mailer)
|
||||
svc := account.NewEmailService(store, mailer, "https://erudit-game.ru")
|
||||
|
||||
acc := provisionAccount(t)
|
||||
email := "user-" + uuid.NewString() + "@example.com"
|
||||
@@ -67,7 +67,7 @@ func TestEmailAlreadyTakenByAnotherAccount(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := account.NewStore(testDB)
|
||||
mailer := &capturingMailer{}
|
||||
svc := account.NewEmailService(store, mailer)
|
||||
svc := account.NewEmailService(store, mailer, "https://erudit-game.ru")
|
||||
|
||||
owner := provisionAccount(t)
|
||||
email := "taken-" + uuid.NewString() + "@example.com"
|
||||
@@ -89,7 +89,7 @@ func TestEmailCodeExpires(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := account.NewStore(testDB)
|
||||
mailer := &capturingMailer{}
|
||||
svc := account.NewEmailService(store, mailer)
|
||||
svc := account.NewEmailService(store, mailer, "https://erudit-game.ru")
|
||||
|
||||
acc := provisionAccount(t)
|
||||
email := "expire-" + uuid.NewString() + "@example.com"
|
||||
@@ -111,7 +111,7 @@ func TestEmailTooManyAttempts(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := account.NewStore(testDB)
|
||||
mailer := &capturingMailer{}
|
||||
svc := account.NewEmailService(store, mailer)
|
||||
svc := account.NewEmailService(store, mailer, "https://erudit-game.ru")
|
||||
|
||||
acc := provisionAccount(t)
|
||||
email := "lock-" + uuid.NewString() + "@example.com"
|
||||
@@ -203,10 +203,10 @@ func TestUpdateProfileOffsetTimezone(t *testing.T) {
|
||||
func TestEmailLoginFlow(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mailer := &capturingMailer{}
|
||||
svc := account.NewEmailService(account.NewStore(testDB), mailer)
|
||||
svc := account.NewEmailService(account.NewStore(testDB), mailer, "https://erudit-game.ru")
|
||||
email := "login-" + uuid.NewString() + "@example.com"
|
||||
|
||||
accountID, err := svc.RequestLoginCode(ctx, email, "+02:00")
|
||||
accountID, err := svc.RequestLoginCode(ctx, email, "+02:00", "en")
|
||||
if err != nil {
|
||||
t.Fatalf("request login code: %v", err)
|
||||
}
|
||||
@@ -233,7 +233,7 @@ func TestEmailLoginFlow(t *testing.T) {
|
||||
}
|
||||
|
||||
// A second login for the same email is the returning user: same account.
|
||||
if _, err := svc.RequestLoginCode(ctx, email, ""); err != nil {
|
||||
if _, err := svc.RequestLoginCode(ctx, email, "", "ru"); err != nil {
|
||||
t.Fatalf("second request: %v", err)
|
||||
}
|
||||
acc2, err := svc.LoginWithCode(ctx, email, sixDigit.FindString(mailer.lastBody))
|
||||
|
||||
Reference in New Issue
Block a user