Skip to content

Commit

Permalink
fix: update email change logic to support secure option
Browse files Browse the repository at this point in the history
  • Loading branch information
kangmingtay committed Sep 23, 2021
1 parent 3289d69 commit 407e20c
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 31 deletions.
23 changes: 22 additions & 1 deletion api/mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ func (a *API) sendMagicLink(tx *storage.Connection, u *models.User, mailer maile
return errors.Wrap(tx.UpdateOnly(u, "recovery_token", "recovery_sent_at"), "Database error updating user for recovery")
}

func (a *API) sendEmailChange(tx *storage.Connection, u *models.User, mailer mailer.Mailer, email string, referrerURL string) error {
// sendSecureEmailChange sends out an email change token each to the old and new emails.
func (a *API) sendSecureEmailChange(tx *storage.Connection, u *models.User, mailer mailer.Mailer, email string, referrerURL string) error {
u.EmailChangeTokenCurrent, u.EmailChangeTokenNew = crypto.SecureToken(), crypto.SecureToken()
u.EmailChange = email
u.EmailChangeConfirmStatus = noneConfirmed
Expand All @@ -249,6 +250,26 @@ func (a *API) sendEmailChange(tx *storage.Connection, u *models.User, mailer mai
), "Database error updating user for email change")
}

// sendEmailChange sends out an email change token to the new email.
func (a *API) sendEmailChange(tx *storage.Connection, u *models.User, mailer mailer.Mailer, email string, referrerURL string) error {
u.EmailChangeTokenNew = crypto.SecureToken()
u.EmailChange = email
u.EmailChangeConfirmStatus = noneConfirmed
now := time.Now()
if err := mailer.EmailChangeMail(u, referrerURL); err != nil {
return err
}

u.EmailChangeSentAt = &now
return errors.Wrap(tx.UpdateOnly(
u,
"email_change_token_new",
"email_change",
"email_change_sent_at",
"email_change_confirm_status",
), "Database error updating user for email change")
}

func (a *API) validateEmail(ctx context.Context, email string) error {
if email == "" {
return unprocessableEntityError("An email address is required")
Expand Down
10 changes: 8 additions & 2 deletions api/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,14 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error {

mailer := a.Mailer(ctx)
referrer := a.getReferrer(r)
if terr = a.sendEmailChange(tx, user, mailer, params.Email, referrer); terr != nil {
return internalServerError("Error sending change email").WithInternalError(terr)
if config.Mailer.SecureEmailChangeEnabled {
if terr = a.sendSecureEmailChange(tx, user, mailer, params.Email, referrer); terr != nil {
return internalServerError("Error sending change email").WithInternalError(terr)
}
} else {
if terr = a.sendEmailChange(tx, user, mailer, params.Email, referrer); terr != nil {
return internalServerError("Error sending change email").WithInternalError(terr)
}
}
}

Expand Down
32 changes: 17 additions & 15 deletions api/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,23 +320,25 @@ func (a *API) emailChangeVerify(ctx context.Context, conn *storage.Connection, p
return nil, expiredTokenError("Email change token expired").WithInternalError(redirectWithQueryError)
}

if user.EmailChangeConfirmStatus == noneConfirmed {
err = a.db.Transaction(func(tx *storage.Connection) error {
user.EmailChangeConfirmStatus = oneConfirmed
if params.Token == user.EmailChangeTokenCurrent {
user.EmailChangeTokenCurrent = ""
} else if params.Token == user.EmailChangeTokenNew {
user.EmailChangeTokenNew = ""
}
if terr := tx.UpdateOnly(user, "email_change_confirm_status", "email_change_token_current", "email_change_token_new"); terr != nil {
return terr
if config.Mailer.SecureEmailChangeEnabled {
if user.EmailChangeConfirmStatus == noneConfirmed {
err = a.db.Transaction(func(tx *storage.Connection) error {
user.EmailChangeConfirmStatus = oneConfirmed
if params.Token == user.EmailChangeTokenCurrent {
user.EmailChangeTokenCurrent = ""
} else if params.Token == user.EmailChangeTokenNew {
user.EmailChangeTokenNew = ""
}
if terr := tx.UpdateOnly(user, "email_change_confirm_status", "email_change_token_current", "email_change_token_new"); terr != nil {
return terr
}
return nil
})
if err != nil {
return nil, err
}
return nil
})
if err != nil {
return nil, err
return nil, acceptedTokenError("Email change request accepted").WithInternalError(redirectWithQueryError)
}
return nil, acceptedTokenError("Email change request accepted").WithInternalError(redirectWithQueryError)
}

// one email is confirmed at this point
Expand Down
45 changes: 32 additions & 13 deletions mailer/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,30 @@ func (m *TemplateMailer) ConfirmationMail(user *models.User, referrerURL string)

// EmailChangeMail sends an email change confirmation mail to a user
func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string) error {
type Email struct {
Address string
Token string
Subject string
Template string
}
emails := []Email{
{
Address: user.EmailChange,
Token: user.EmailChangeTokenNew,
Subject: string(withDefault(m.Config.Mailer.Subjects.EmailChange, "Confirm Email Change")),
Template: m.Config.Mailer.Templates.Confirmation,
},
}

if m.Config.Mailer.SecureEmailChangeEnabled {
emails = append(emails, Email{
Address: user.GetEmail(),
Token: user.EmailChangeTokenCurrent,
Subject: string(withDefault(m.Config.Mailer.Subjects.Confirmation, "Confirm Email Address")),
Template: m.Config.Mailer.Templates.EmailChange,
})
}

globalConfig, err := conf.LoadGlobal(configFile)
if err != nil {
return err
Expand All @@ -120,42 +144,37 @@ func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string)
if len(referrerURL) > 0 {
redirectParam = "&redirect_to=" + referrerURL
}

errors := make(chan error)
tokens := map[string]string{
user.GetEmail(): user.EmailChangeTokenCurrent,
user.EmailChange: user.EmailChangeTokenNew,
}
for email, token := range tokens {
for _, email := range emails {
url, err := getSiteURL(
referrerURL,
globalConfig.API.ExternalURL,
m.Config.Mailer.URLPaths.EmailChange,
"token="+token+"&type=email_change"+redirectParam,
"token="+email.Token+"&type=email_change"+redirectParam,
)
if err != nil {
return err
}
go func(e, t string) {
go func(address, token, template string) {
data := map[string]interface{}{
"SiteURL": m.Config.SiteURL,
"ConfirmationURL": url,
"Email": user.GetEmail(),
"NewEmail": user.EmailChange,
"Token": t,
"Token": token,
"Data": user.UserMetaData,
}
errors <- m.Mailer.Mail(
e,
address,
string(withDefault(m.Config.Mailer.Subjects.EmailChange, "Confirm Email Change")),
m.Config.Mailer.Templates.EmailChange,
template,
defaultEmailChangeMail,
data,
)
}(email, token)
}(email.Address, email.Token, email.Template)
}

for i := 0; i < len(tokens); i++ {
for i := 0; i < len(emails); i++ {
e := <-errors
if e != nil {
return e
Expand Down

0 comments on commit 407e20c

Please sign in to comment.