Provide email during signup

This commit is contained in:
binwiederhier
2026-06-12 23:13:01 -04:00
parent f558935c1e
commit 1215e99098
7 changed files with 98 additions and 7 deletions
+13
View File
@@ -37,6 +37,9 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
if err != nil {
return err
}
if newAccount.Email != "" && !emailAddressRegex.MatchString(newAccount.Email) {
return errHTTPBadRequestEmailAddressInvalid
}
if existingUser, _ := s.userManager.User(newAccount.Username); existingUser != nil {
return errHTTPConflictUserExists
}
@@ -48,6 +51,16 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
return err
}
v.AccountCreated()
// If an email was provided and email sending is configured, start verification (best-effort).
// The address becomes the primary email on verify (the new account has no primary yet); a
// failure to send must not fail signup, so we only log it.
if newAccount.Email != "" && s.mailSender != nil {
if u, err := s.userManager.User(newAccount.Username); err != nil {
logvr(v, r).Tag(tagAccount).Err(err).Warn("Failed to load new user for email verification")
} else if err := s.enqueueEmailVerification(u.ID, newAccount.Email); err != nil {
logvr(v, r).Tag(tagAccount).Err(err).Warn("Failed to send signup email verification")
}
}
return s.writeJSON(w, newSuccessResponse())
}
+50
View File
@@ -276,6 +276,56 @@ func TestAccount_PasswordReset_NoPrimaryEmailNoSend(t *testing.T) {
})
}
func TestAccount_Signup_WithEmail_SendsVerification(t *testing.T) {
forEachBackend(t, func(t *testing.T, databaseURL string) {
conf := newTestConfigWithAuthFile(t, databaseURL)
conf.EnableSignup = true
conf.SMTPSenderAddr = "localhost:25"
conf.SMTPSenderFrom = "noreply@example.com"
conf.BaseURL = "https://ntfy.example.com"
s := newTestServer(t, conf)
mailer := newCaptureMailer()
s.mailSender = mailer
defer s.closeDatabases()
// Sign up with an optional email -> account created and a verification link sent
rr := request(t, s, "POST", "/v1/account", `{"username":"emma","password":"emmapass","email":"emma@example.com"}`, nil)
require.Equal(t, 200, rr.Code)
link := mailer.verifyLinks["emma@example.com"]
require.NotEmpty(t, link)
// Verifying the link makes it the (first) primary email
token := tokenFromLink(t, link, "https://ntfy.example.com/account/email/verify/")
require.Equal(t, 200, request(t, s, "POST", "/v1/account/email/verify", fmt.Sprintf(`{"token":"%s"}`, token), nil).Code)
account := getAccount(t, s, map[string]string{"Authorization": util.BasicAuth("emma", "emmapass")})
require.Equal(t, []string{"emma@example.com"}, verifiedAddrs(account))
require.Equal(t, "emma@example.com", primaryAddr(account))
})
}
func TestAccount_Signup_WithoutEmail_NoSend(t *testing.T) {
forEachBackend(t, func(t *testing.T, databaseURL string) {
conf := newTestConfigWithAuthFile(t, databaseURL)
conf.EnableSignup = true
conf.SMTPSenderAddr = "localhost:25"
conf.SMTPSenderFrom = "noreply@example.com"
conf.BaseURL = "https://ntfy.example.com"
s := newTestServer(t, conf)
mailer := newCaptureMailer()
s.mailSender = mailer
defer s.closeDatabases()
// No email -> account created, nothing sent
require.Equal(t, 200, request(t, s, "POST", "/v1/account", `{"username":"emma","password":"emmapass"}`, nil).Code)
require.Empty(t, mailer.verifyLinks)
// Invalid email -> rejected
rr := request(t, s, "POST", "/v1/account", `{"username":"otto","password":"ottopass","email":"not-an-email"}`, nil)
require.Equal(t, 400, rr.Code)
require.Equal(t, 40050, toHTTPError(t, rr.Body.String()).Code)
})
}
func TestAccount_Email_ProvisionedNoPrimary(t *testing.T) {
forEachBackend(t, func(t *testing.T, databaseURL string) {
hash, err := user.HashPassword("provpass")
+1
View File
@@ -185,6 +185,7 @@ type apiAccessResetRequest struct {
type apiAccountCreateRequest struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"` // Optional; if set (and SMTP configured), a verification link is sent
}
type apiAccountPasswordChangeRequest struct {