diff --git a/api/hooks.go b/api/hooks.go index 39ddba9b26..2223f5da62 100644 --- a/api/hooks.go +++ b/api/hooks.go @@ -31,6 +31,7 @@ const ( gotrueIssuer = "gotrue" ValidateEvent = "validate" SignupEvent = "signup" + EmailChangeEvent = "email_change" LoginEvent = "login" ) diff --git a/api/user.go b/api/user.go index d3c8264f09..cf3e37039d 100644 --- a/api/user.go +++ b/api/user.go @@ -12,11 +12,10 @@ import ( // UserUpdateParams parameters for updating a user type UserUpdateParams struct { - Email string `json:"email"` - Password string `json:"password"` - EmailChangeToken string `json:"email_change_token"` - Data map[string]interface{} `json:"data"` - AppData map[string]interface{} `json:"app_metadata,omitempty"` + Email string `json:"email"` + Password string `json:"password"` + Data map[string]interface{} `json:"data"` + AppData map[string]interface{} `json:"app_metadata,omitempty"` } // UserGet returns a user @@ -106,17 +105,7 @@ func (a *API) UserUpdate(w http.ResponseWriter, r *http.Request) error { } } - if params.EmailChangeToken != "" { - log.Debugf("Got change token %v", params.EmailChangeToken) - - if params.EmailChangeToken != user.EmailChangeToken { - return unauthorizedError("Email Change Token didn't match token on file") - } - - if terr = user.ConfirmEmailChange(tx); terr != nil { - return internalServerError("Error updating user").WithInternalError(terr) - } - } else if params.Email != "" && params.Email != user.Email { + if params.Email != "" && params.Email != user.Email { if terr = a.validateEmail(ctx, params.Email); terr != nil { return terr } diff --git a/api/verify.go b/api/verify.go index 405e78ad7d..7b1c54677e 100644 --- a/api/verify.go +++ b/api/verify.go @@ -20,10 +20,11 @@ var ( ) const ( - signupVerification = "signup" - recoveryVerification = "recovery" - inviteVerification = "invite" - magicLinkVerification = "magiclink" + signupVerification = "signup" + recoveryVerification = "recovery" + inviteVerification = "invite" + magicLinkVerification = "magiclink" + emailChangeVerification = "email_change" ) // VerifyParams are the parameters the Verify endpoint accepts @@ -77,6 +78,8 @@ func (a *API) Verify(w http.ResponseWriter, r *http.Request) error { user, terr = a.signupVerify(ctx, tx, params) case recoveryVerification, magicLinkVerification: user, terr = a.recoverVerify(ctx, tx, params) + case emailChangeVerification: + user, terr = a.emailChangeVerify(ctx, tx, params) default: return unprocessableEntityError("Verify requires a verification type") } @@ -242,3 +245,47 @@ func (a *API) prepErrorRedirectURL(err *HTTPError, r *http.Request) string { q.Set("error_description", err.Message) return rurl + "#" + q.Encode() } + +func (a *API) emailChangeVerify(ctx context.Context, conn *storage.Connection, params *VerifyParams) (*models.User, error) { + instanceID := getInstanceID(ctx) + config := a.getConfig(ctx) + user, err := models.FindUserByEmailChangeToken(conn, params.Token) + if err != nil { + if models.IsNotFoundError(err) { + return nil, notFoundError(err.Error()).WithInternalError(redirectWithQueryError) + } + return nil, internalServerError("Database error finding user").WithInternalError(err) + } + + nextDay := user.EmailChangeSentAt.Add(24 * time.Hour) + if user.EmailChangeSentAt != nil && time.Now().After(nextDay) { + return nil, expiredTokenError("Recovery token expired").WithInternalError(redirectWithQueryError) + } + + err = a.db.Transaction(func(tx *storage.Connection) error { + var terr error + + if params.Token != user.EmailChangeToken { + return unauthorizedError("Email Change Token didn't match token on file") + } + + if terr = models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, nil); terr != nil { + return terr + } + + if terr = triggerEventHooks(ctx, tx, EmailChangeEvent, user, instanceID, config); terr != nil { + return terr + } + + if terr = user.ConfirmEmailChange(tx); terr != nil { + return internalServerError("Error confirm email").WithInternalError(terr) + } + + return nil + }) + if err != nil { + return nil, err + } + + return user, nil +} diff --git a/mailer/template.go b/mailer/template.go index ee6e7c9164..bf3a941d32 100644 --- a/mailer/template.go +++ b/mailer/template.go @@ -114,7 +114,7 @@ func (m *TemplateMailer) EmailChangeMail(user *models.User, referrerURL string) redirectParam = "&redirect_to=" + referrerURL } - url, err := getSiteURL(referrerURL, m.Config.SiteURL, m.Config.Mailer.URLPaths.EmailChange, "email_change_token="+user.EmailChangeToken+"&type=email_change"+redirectParam) + url, err := getSiteURL(referrerURL, m.Config.SiteURL, m.Config.Mailer.URLPaths.EmailChange, "token="+user.EmailChangeToken+"&type=email_change"+redirectParam) if err != nil { return err } diff --git a/models/user.go b/models/user.go index 67608c5b77..ffd6816de7 100644 --- a/models/user.go +++ b/models/user.go @@ -279,6 +279,11 @@ func FindUserByRecoveryToken(tx *storage.Connection, token string) (*User, error return findUser(tx, "recovery_token = ?", token) } +// FindUserByRecoveryToken finds a user with the matching recovery token. +func FindUserByEmailChangeToken(tx *storage.Connection, token string) (*User, error) { + return findUser(tx, "email_change_token = ?", token) +} + // FindUserWithRefreshToken finds a user from the provided refresh token. func FindUserWithRefreshToken(tx *storage.Connection, token string) (*User, *RefreshToken, error) { refreshToken := &RefreshToken{}