diff --git a/api/handlers/cards.go b/api/handlers/cards.go index 8f05b08..436ae3d 100644 --- a/api/handlers/cards.go +++ b/api/handlers/cards.go @@ -7,7 +7,7 @@ import ( "fmt" "net/http" "strconv" - "sync" + "time" "github.com/go-chi/chi/v5" "github.com/stripe/stripe-go/v78" @@ -24,35 +24,43 @@ import ( stripeSession "github.com/stripe/stripe-go/v78/checkout/session" ) -// payRedirects maps short tokens to full Stripe checkout URLs. -var ( - payRedirects = map[string]string{} - payRedirectsMu sync.Mutex -) +const RedirectTTL = 10 * time.Minute -func storeRedirect(stripeURL string) (string, error) { +func storeRedirect(stripeURL, purpose string) (string, error) { b := make([]byte, 16) if _, err := rand.Read(b); err != nil { return "", err } token := hex.EncodeToString(b) - payRedirectsMu.Lock() - payRedirects[token] = stripeURL - payRedirectsMu.Unlock() + now := time.Now() + rec := models.PayRedirect{ + Token: token, + URL: stripeURL, + Purpose: purpose, + CreatedAt: now, + ExpiresAt: now.Add(RedirectTTL), + } + if err := database.GetDB().Create(&rec).Error; err != nil { + return "", err + } return token, nil } // PayRedirect resolves a short token and redirects to the Stripe checkout URL. func PayRedirect(w http.ResponseWriter, r *http.Request) { token := chi.URLParam(r, "token") - payRedirectsMu.Lock() - url, ok := payRedirects[token] - payRedirectsMu.Unlock() - if !ok { + db := database.GetDB().WithContext(r.Context()) + var rec models.PayRedirect + if err := db.Where("token = ?", token).First(&rec).Error; err != nil { http.NotFound(w, r) return } - http.Redirect(w, r, url, http.StatusFound) + if time.Now().After(rec.ExpiresAt) { + db.Where("token = ?", rec.Token).Delete(&models.PayRedirect{}) + http.NotFound(w, r) + return + } + http.Redirect(w, r, rec.URL, http.StatusFound) } // CardHandler handles saved payment method CRUD. @@ -168,7 +176,7 @@ func (h *CardHandler) CollectCard(w http.ResponseWriter, r *http.Request) { return } - token, err := storeRedirect(sess.URL) + token, err := storeRedirect(sess.URL, models.RedirectPurposeAddCard) if err != nil { utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to generate redirect token", nil) return diff --git a/api/handlers/cart.go b/api/handlers/cart.go index 18deca0..1019eed 100644 --- a/api/handlers/cart.go +++ b/api/handlers/cart.go @@ -541,7 +541,7 @@ func (h *CartHandler) respondRequiresAction(w http.ResponseWriter, order *models return } - token, err := storeRedirect(pi.NextAction.RedirectToURL.URL) + token, err := storeRedirect(pi.NextAction.RedirectToURL.URL, models.RedirectPurposeCheckout3DS) if err != nil { utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to allocate redirect token", nil) return diff --git a/pkg/database/migrate.go b/pkg/database/migrate.go index 1ba5131..137d988 100644 --- a/pkg/database/migrate.go +++ b/pkg/database/migrate.go @@ -3,6 +3,7 @@ package database import ( "fmt" "log" + "terminalShop/pkg/models" "gorm.io/gorm" @@ -24,6 +25,7 @@ func Migrate(db *gorm.DB) error { &models.OrderItem{}, // Line items within orders &models.Address{}, // Saved addresses &models.OrderEvent{}, + &models.PayRedirect{}, // short-token -> stripe url store (10 min TTL) ); err != nil { return fmt.Errorf("migration failed: %w", err) } diff --git a/pkg/models/pay_redirect.go b/pkg/models/pay_redirect.go new file mode 100644 index 0000000..b0eb64d --- /dev/null +++ b/pkg/models/pay_redirect.go @@ -0,0 +1,23 @@ +package models + +import "time" + +// PayRedirect maps a short token to a stripe hosted url with a TTL. +// survives api restarts so a customer mid-3Ds authentication still +// lands on the bank-hosted challenge page after a deploy or crash +type PayRedirect struct { + Token string `gorm:"primaryKey;size:64" json:"token"` + URL string `gorm:"type:text;not null" json:"url"` + Purpose string `gorm:"size:32;not null" json:"purpose"` + CreatedAt time.Time `json:"created_at"` + ExpiresAt time.Time `gorm:"index" json:"expires_at"` +} + +const ( + RedirectPurposeCheckout3DS = "checkout_3ds" + RedirectPurposeAddCard = "add_card" +) + +func (PayRedirect) TableName() string { + return "pay_redirects" +}