Skip to content

Commit

Permalink
(BIDS-2428) improve email change flow
Browse files Browse the repository at this point in the history
  • Loading branch information
LuccaBitfly committed Nov 7, 2023
1 parent f5ef71f commit 325eedb
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 62 deletions.
11 changes: 11 additions & 0 deletions db/migrations/temp_add_stripe_update_column.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query - add column stripe_email_pending';
ALTER TABLE users ADD COLUMN IF NOT EXISTS stripe_email_pending BOOLEAN NOT NULL DEFAULT FALSE;
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query - remove column stripe_email_pending';
ALTER TABLE users DROP COLUMN IF EXISTS stripe_email_pending;
-- +goose StatementEnd
73 changes: 11 additions & 62 deletions handlers/user.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package handlers

import (
"bytes"
"database/sql"
"encoding/hex"
"encoding/json"
Expand Down Expand Up @@ -1349,7 +1348,7 @@ func UserUpdateEmailPost(w http.ResponseWriter, r *http.Request) {
}

// validate new email
newEmail := r.FormValue("email")
newEmail := strings.ToLower(r.FormValue("email"))

if userData.Email == newEmail {
session.Save(r, w)
Expand All @@ -1375,7 +1374,7 @@ func UserUpdateEmailPost(w http.ResponseWriter, r *http.Request) {
}

if emailExists {
session.AddFlash("Error: Email already exists, please choose a unique email")
session.AddFlash(authInternalServerErrorFlashMsg)
session.Save(r, w)
http.Redirect(w, r, "/user/settings", http.StatusSeeOther)
return
Expand All @@ -1392,7 +1391,7 @@ func UserUpdateEmailPost(w http.ResponseWriter, r *http.Request) {
return
}

session.AddFlash("Verification link sent to your new email " + newEmail)
session.AddFlash("An email has been sent, please click the link in the email to confirm your email change. The link will expire in 30 minutes.")
session.Save(r, w)
http.Redirect(w, r, "/user/settings", http.StatusSeeOther)
}
Expand Down Expand Up @@ -1434,11 +1433,6 @@ func UserConfirmUpdateEmail(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if !sessionUser.Authenticated {
utils.SetFlash(w, r, authSessionName, "Error: You need to be logged in to update your email. Please login and try again.")
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}

user := struct {
ID int64 `db:"id"`
Expand Down Expand Up @@ -1470,12 +1464,6 @@ func UserConfirmUpdateEmail(w http.ResponseWriter, r *http.Request) {

// validate data

if sessionUser.UserID != uint64(user.ID) {
utils.SetFlash(w, r, authSessionName, "Error: This link is invalid / outdated.")
http.Redirect(w, r, "/user/settings", http.StatusSeeOther)
return
}

if !user.Confirmed {
utils.SetFlash(w, r, authSessionName, "Error: Cannot update email for an unconfirmed address. Please confirm your email first.")
http.Redirect(w, r, "/confirmation", http.StatusSeeOther)
Expand Down Expand Up @@ -1504,35 +1492,25 @@ func UserConfirmUpdateEmail(w http.ResponseWriter, r *http.Request) {
}

if emailExists {
utils.SetFlash(w, r, authSessionName, "Error: Could not update email. The new email already exists, please send a new update request with a different email.")
utils.SetFlash(w, r, authSessionName, "Error: Could not update email. The new email already exists, please send a request with a different email.")
http.Redirect(w, r, "/user/settings", http.StatusSeeOther)
return
}

// everything is fine, update email
// update users email in Stripe, if user has a subscription
if user.StripeCustomerId != "" {
err = updateStripeCustomerEmail(user.StripeCustomerId, user.NewEmail)
if err != nil {
utils.LogError(err, "error updating user email in stripe", 0, map[string]interface{}{"customerID": user.StripeCustomerId, "newEmail": user.NewEmail})
utils.SetFlash(w, r, authSessionName, "Error: Could not update email. Please try again. If this error persists please contact <a href=\"https://support.bitfly.at/support/home\">support</a>.")
http.Redirect(w, r, "/user/settings", http.StatusSeeOther)
}
}

// update users email in DB
// if this fails, the user will not be able to log in with the new email but stripe will have the new email
_, err = db.FrontendWriterDB.Exec(`UPDATE users SET email = $1, email_confirmation_hash = NULL, email_change_to_value = NULL WHERE id = $2`, user.NewEmail, user.ID)
// update users email in DB and set stripe email pending flag
_, err = db.FrontendWriterDB.Exec(`UPDATE users SET email = $1, email_confirmation_hash = NULL, email_change_to_value = NULL, stripe_email_pending = $2 WHERE id = $3`, user.NewEmail, user.StripeCustomerId != "", user.ID)
if err != nil {
utils.LogError(err, "error updating email for user", 0, map[string]interface{}{"userID": user.ID, "newEmail": user.NewEmail})
utils.SetFlash(w, r, authSessionName, "Error: Could not update email. Please try again. If this error persists please contact <a href=\"https://support.bitfly.at/support/home\">support</a>.")
http.Redirect(w, r, "/user/settings", http.StatusSeeOther)
return
}

session.SetValue("subscription", "")
session.SetValue("authenticated", false)
session.DeleteValue("user_id")
if sessionUser.UserID == uint64(user.ID) {
session.SetValue("subscription", "")
session.SetValue("authenticated", false)
session.DeleteValue("user_id")
}

err = purgeAllSessionsForUser(r.Context(), uint64(user.ID))
if err != nil {
Expand All @@ -1546,35 +1524,6 @@ func UserConfirmUpdateEmail(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/login", http.StatusSeeOther)
}

func updateStripeCustomerEmail(stripeCustomerId, newEmail string) error {
// see https://stripe.com/docs/api/customers/update
apiEndpoint := "https://api.stripe.com/v1/customers/" + stripeCustomerId

data := url.Values{}
data.Set("email", newEmail)
req, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewBufferString(data.Encode()))
if err != nil {
return fmt.Errorf("error creating email change request for stripe: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", utils.Config.Frontend.Stripe.SecretKey))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

httpClient := http.Client{Timeout: time.Second * 10}
resp, err := httpClient.Do(req)
if err != nil {
return fmt.Errorf("error sending request to stripe: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("error updating email in stripe, also could not read body: %w", err)
}
return fmt.Errorf("error updating email in stripe: %w; body: %v", err, string(body))
}
return nil
}

// UserValidatorWatchlistAdd godoc
// @Summary subscribes a user to get notifications from a specific validator
// @Tags User
Expand Down
3 changes: 3 additions & 0 deletions services/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ func Init() {
ready.Add(1)
go latestExportedStatisticDayUpdater(ready)

ready.Add(1)
go stripeEmailUpdater(ready)

ready.Wait()
}

Expand Down
92 changes: 92 additions & 0 deletions services/stripe_email_updater.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package services

import (
"bytes"
"eth2-exporter/db"
"eth2-exporter/utils"
"fmt"
"io"
"net/http"
"net/url"
"sync"
"time"

"github.com/lib/pq"
)

func stripeEmailUpdater(wg *sync.WaitGroup) {
firstRun := true
for {
// fetch all users with pending stripe email updates
var pendingUsers []struct {
Email string `db:"email"`
StripeCustomerId string `db:"stripe_customer_id"`
}
err := db.FrontendReaderDB.Select(&pendingUsers, "SELECT email, COALESCE(stripe_customer_id. '') FROM users WHERE stripe_email_pending")
if err != nil {
utils.LogError(err, "error getting pending users for stripe email update service", 0)
time.Sleep(time.Second * 10)
continue
}

// update stripe customer email
var updatedUsers []string
for _, user := range pendingUsers {
if user.StripeCustomerId == "" {
utils.LogError(fmt.Errorf("user has no stripe_customer_id"), "error updating stripe customer email, this should never happen", 0, map[string]interface{}{"email": user.Email})
continue
}
err := updateStripeCustomerEmail(user.StripeCustomerId, user.Email)
if err != nil {
utils.LogError(err, "error updating stripe customer email", 0, map[string]interface{}{"email": user.Email, "stripe_customer_id": user.StripeCustomerId})
continue
}
updatedUsers = append(updatedUsers, user.Email)
}

// set stripe_email_pending flag to false for all users that were updated
if len(updatedUsers) > 0 {
_, err := db.FrontendWriterDB.Exec("UPDATE users SET stripe_email_pending = false WHERE email = ANY($1)", pq.Array(updatedUsers))
if err != nil {
utils.LogError(err, "error setting stripe_email_pending flag false for users, stripe email was updated", 0, map[string]interface{}{"emails": updatedUsers})
time.Sleep(time.Second * 10)
continue
}
}

if firstRun {
wg.Done()
firstRun = false
}
time.Sleep(time.Minute)
}
}

func updateStripeCustomerEmail(stripeCustomerId, newEmail string) error {
// see https://stripe.com/docs/api/customers/update
apiEndpoint := "https://api.stripe.com/v1/customers/" + stripeCustomerId

data := url.Values{}
data.Set("email", newEmail)
req, err := http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewBufferString(data.Encode()))
if err != nil {
return fmt.Errorf("error creating email change request for stripe: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", utils.Config.Frontend.Stripe.SecretKey))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

httpClient := http.Client{Timeout: time.Second * 10}
resp, err := httpClient.Do(req)
if err != nil {
return fmt.Errorf("error sending request to stripe: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("error updating email in stripe, also could not read body: %w", err)
}
return fmt.Errorf("error updating email in stripe: %w; body: %v", err, string(body))
}
return nil
}

0 comments on commit 325eedb

Please sign in to comment.