Compare commits

..

2 Commits

6 changed files with 22 additions and 56 deletions

View File

@@ -8,9 +8,9 @@ jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
@@ -30,7 +30,6 @@ jobs:
fi
- name: Lint
if: ${{ !env.ACT }}
uses: golangci/golangci-lint-action@v7
with:
version: v2.4.0

View File

@@ -8,10 +8,10 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
go-version: '1.23'
- name: Run tests
run: go test ./...
- name: Deploy to server

View File

@@ -7,7 +7,7 @@ import (
"fmt"
"net/http"
"strconv"
"time"
"sync"
"github.com/go-chi/chi/v5"
"github.com/stripe/stripe-go/v78"
@@ -24,43 +24,35 @@ import (
stripeSession "github.com/stripe/stripe-go/v78/checkout/session"
)
const RedirectTTL = 10 * time.Minute
// payRedirects maps short tokens to full Stripe checkout URLs.
var (
payRedirects = map[string]string{}
payRedirectsMu sync.Mutex
)
func storeRedirect(stripeURL, purpose string) (string, error) {
func storeRedirect(stripeURL string) (string, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
token := hex.EncodeToString(b)
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
}
payRedirectsMu.Lock()
payRedirects[token] = stripeURL
payRedirectsMu.Unlock()
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")
db := database.GetDB().WithContext(r.Context())
var rec models.PayRedirect
if err := db.Where("token = ?", token).First(&rec).Error; err != nil {
payRedirectsMu.Lock()
url, ok := payRedirects[token]
payRedirectsMu.Unlock()
if !ok {
http.NotFound(w, r)
return
}
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)
http.Redirect(w, r, url, http.StatusFound)
}
// CardHandler handles saved payment method CRUD.
@@ -176,7 +168,7 @@ func (h *CardHandler) CollectCard(w http.ResponseWriter, r *http.Request) {
return
}
token, err := storeRedirect(sess.URL, models.RedirectPurposeAddCard)
token, err := storeRedirect(sess.URL)
if err != nil {
utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to generate redirect token", nil)
return

View File

@@ -541,7 +541,7 @@ func (h *CartHandler) respondRequiresAction(w http.ResponseWriter, order *models
return
}
token, err := storeRedirect(pi.NextAction.RedirectToURL.URL, models.RedirectPurposeCheckout3DS)
token, err := storeRedirect(pi.NextAction.RedirectToURL.URL)
if err != nil {
utils.RespondError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to allocate redirect token", nil)
return

View File

@@ -3,7 +3,6 @@ package database
import (
"fmt"
"log"
"terminalShop/pkg/models"
"gorm.io/gorm"
@@ -25,7 +24,6 @@ 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)
}

View File

@@ -1,23 +0,0 @@
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"
}