db telemetry

This commit is contained in:
2026-05-02 16:29:07 +02:00
parent 327bb9c867
commit 52d58583e9
16 changed files with 241 additions and 98 deletions

View File

@@ -35,7 +35,7 @@ func NewAddressHandler(shippoAPIKey, bringAPIUID, bringAPIKey string) *AddressHa
// Retrieve all saved addresses for a user
func (h *AddressHandler) GetAddresses(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
@@ -48,7 +48,7 @@ func (h *AddressHandler) GetAddresses(w http.ResponseWriter, r *http.Request) {
}
func (h *AddressHandler) CreateAddress(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
@@ -82,7 +82,7 @@ func (h *AddressHandler) CreateAddress(w http.ResponseWriter, r *http.Request) {
return
}
client := shippo.NewClient(h.shippoKey)
validated, err := client.ValidateAddress(shippo.Address{
validated, err := client.ValidateAddress(r.Context(), shippo.Address{
Name: req.Name,
Street1: req.Street,
Street2: req.Street2,
@@ -114,7 +114,7 @@ func (h *AddressHandler) CreateAddress(w http.ResponseWriter, r *http.Request) {
return
}
client := bring.NewClient(h.bringAPIUID, h.bringAPIKey)
validated, err := client.ValidateAddress(bring.Address{
validated, err := client.ValidateAddress(r.Context(), bring.Address{
Name: req.Name,
Street1: req.Street,
Street2: req.Street2,
@@ -161,7 +161,7 @@ func (h *AddressHandler) CreateAddress(w http.ResponseWriter, r *http.Request) {
// DeleteAddress deltes a saved address
func (h *AddressHandler) DeleteAddress(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
idStr := chi.URLParam(r, "id")
@@ -191,7 +191,7 @@ func (h *AddressHandler) DeleteAddress(w http.ResponseWriter, r *http.Request) {
// Mark an address as the users default address
func (h *AddressHandler) SetDefaultAddress(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
idStr := chi.URLParam(r, "id")

View File

@@ -46,7 +46,7 @@ type TokenResponse struct {
}
func (h *AuthHandler) GetToken(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
var req TokenRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -127,7 +127,7 @@ type RegisterRequest struct {
// RegisterWithSSHKey registers a new user with their SSH public key
// No username required - following terminal.shop pattern
func (h *AuthHandler) RegisterWithSSHKey(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
var req RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -180,7 +180,7 @@ func (h *AuthHandler) RegisterWithSSHKey(w http.ResponseWriter, r *http.Request)
// GetUserBySSHKey retrieves a user by their SSH key fingerprint
func (h *AuthHandler) GetUserBySSHKey(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
fingerprint := r.URL.Query().Get("fingerprint")
if fingerprint == "" {

View File

@@ -66,7 +66,7 @@ func NewCardHandler(stripeSecretKey string, appURL string) *CardHandler {
// GetCards returns all saved cards for the authenticated user.
func (h *CardHandler) GetCards(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
var cards []models.Card
@@ -81,7 +81,7 @@ func (h *CardHandler) GetCards(w http.ResponseWriter, r *http.Request) {
// GetCard returns a single saved card by ID.
func (h *CardHandler) GetCard(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
idStr := chi.URLParam(r, "id")
@@ -103,7 +103,7 @@ func (h *CardHandler) GetCard(w http.ResponseWriter, r *http.Request) {
}
func (h *CardHandler) SetDefaultCard(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
idStr := chi.URLParam(r, "id")
@@ -136,7 +136,7 @@ func (h *CardHandler) SetDefaultCard(w http.ResponseWriter, r *http.Request) {
}
func (h *CardHandler) CollectCard(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
var user models.User
@@ -184,7 +184,7 @@ type SaveCardRequest struct {
// fingerprint, attaches to the user's Stripe customer, and saves the
// card metadata locally. No raw card data is stored at any point.
func (h *CardHandler) SaveCard(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
var req SaveCardRequest
@@ -280,7 +280,7 @@ func (h *CardHandler) SaveCard(w http.ResponseWriter, r *http.Request) {
// DeleteCard removes a saved card. If the card is currently set on the user's
// cart, the cart's card selection is cleared.
func (h *CardHandler) DeleteCard(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
idStr := chi.URLParam(r, "id")
@@ -345,7 +345,7 @@ func ensureStripeCustomer(gdb *gorm.DB, user *models.User) error {
}
user.StripeCustomerID = cust.ID
if err := gdb.Save(user).Error; err != nil {
return fmt.Errorf("Failed to save stripe customer: %w", err)
return fmt.Errorf("failed to save stripe customer: %w", err)
}
return nil
}

View File

@@ -1,6 +1,7 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
@@ -30,8 +31,8 @@ func NewCartHandler(stripeSecretKey string) *CartHandler {
}
// getOrCreateCart finds the users cart or creates one.
func getOrCreateCart(userID uint) (*models.Cart, error) {
db := database.GetDB()
func getOrCreateCart(ctx context.Context, userID uint) (*models.Cart, error) {
db := database.GetDB().WithContext(ctx)
var cart models.Cart
err := db.Where("user_id = ?", userID).First(&cart).Error
if err != nil {
@@ -43,8 +44,8 @@ func getOrCreateCart(userID uint) (*models.Cart, error) {
return &cart, nil
}
func cartResponse(cart *models.Cart) map[string]interface{} {
db := database.GetDB()
func cartResponse(ctx context.Context, cart *models.Cart) map[string]interface{} {
db := database.GetDB().WithContext(ctx)
var items []models.CartItem
db.Where("cart_id = ? AND quantity > 0", cart.ID).Preload("Coffee").Find(&items)
@@ -76,14 +77,14 @@ func cartResponse(cart *models.Cart) map[string]interface{} {
func (h *CartHandler) GetCart(w http.ResponseWriter, r *http.Request) {
userID := middleware.UserIDFromContext(r.Context())
cart, err := getOrCreateCart(userID)
cart, err := getOrCreateCart(r.Context(), userID)
if err != nil {
utils.RespondError(w, http.StatusInternalServerError, "CART_ERROR", "failed to get cart", nil)
return
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
"cart": cartResponse(cart),
"cart": cartResponse(r.Context(), cart),
})
}
@@ -93,7 +94,7 @@ type SetItemRequest struct {
}
func (h *CartHandler) SetItem(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
var req SetItemRequest
@@ -113,7 +114,7 @@ func (h *CartHandler) SetItem(w http.ResponseWriter, r *http.Request) {
return
}
cart, err := getOrCreateCart(userID)
cart, err := getOrCreateCart(r.Context(), userID)
if err != nil {
utils.RespondError(w, http.StatusInternalServerError, "CART_ERROR", "failed to get cart", nil)
return
@@ -146,7 +147,7 @@ func (h *CartHandler) SetItem(w http.ResponseWriter, r *http.Request) {
}
utils.RespondSuccess(w, http.StatusOK, map[string]interface{}{
"cart": cartResponse(cart),
"cart": cartResponse(r.Context(), cart),
})
}
@@ -155,7 +156,7 @@ type SetAddressRequest struct {
}
func (h *CartHandler) SetAddress(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
var req SetAddressRequest
@@ -170,7 +171,7 @@ func (h *CartHandler) SetAddress(w http.ResponseWriter, r *http.Request) {
return
}
cart, err := getOrCreateCart(userID)
cart, err := getOrCreateCart(r.Context(), userID)
if err != nil {
utils.RespondError(w, http.StatusInternalServerError, "CART_ERROR", "failed to get cart", nil)
return
@@ -191,7 +192,7 @@ type SetCardRequest struct {
}
func (h *CartHandler) SetCard(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
var req SetCardRequest
@@ -206,7 +207,7 @@ func (h *CartHandler) SetCard(w http.ResponseWriter, r *http.Request) {
return
}
cart, err := getOrCreateCart(userID)
cart, err := getOrCreateCart(r.Context(), userID)
if err != nil {
utils.RespondError(w, http.StatusInternalServerError, "CART_ERROR", "failed to get cart", nil)
return
@@ -223,10 +224,10 @@ func (h *CartHandler) SetCard(w http.ResponseWriter, r *http.Request) {
}
func (h *CartHandler) ClearCart(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
cart, err := getOrCreateCart(userID)
cart, err := getOrCreateCart(r.Context(), userID)
if err != nil {
utils.RespondError(w, http.StatusInternalServerError, "CART_ERROR", "failed to get cart", nil)
return
@@ -243,14 +244,14 @@ func (h *CartHandler) ClearCart(w http.ResponseWriter, r *http.Request) {
}
func (h *CartHandler) ConvertCart(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
ctx, span := otel.Tracer("api").Start(r.Context(), "cart.convert")
defer span.End()
r = r.WithContext(ctx)
cart, err := getOrCreateCart(userID)
cart, err := getOrCreateCart(r.Context(), userID)
if err != nil {
middleware.RecordCartConversion("cart_lookup_failed")
utils.RespondError(w, http.StatusInternalServerError, "CART_ERROR", "failed to get cart", nil)

View File

@@ -24,7 +24,7 @@ func NewOrderHandler(stripeSecretKey string) *OrderHandler {
}
func (h *OrderHandler) GetOrders(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
var orders []models.Order
@@ -38,7 +38,7 @@ func (h *OrderHandler) GetOrders(w http.ResponseWriter, r *http.Request) {
// RefundOrder issues a full refund for an order via Stripe and marks it as
// refunded. Only orders in paid or shipped status can be refunded.
func (h *OrderHandler) RefundOrder(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
idStr := chi.URLParam(r, "id")

View File

@@ -20,7 +20,7 @@ func NewProductHandler() *ProductHandler {
// GetProducts returns all products from the database
func (h *ProductHandler) GetProducts(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
var products []models.Coffee
@@ -37,7 +37,7 @@ func (h *ProductHandler) GetProducts(w http.ResponseWriter, r *http.Request) {
// GetProduct returns a single product by ID
func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
// Get ID from URL parameter
idStr := chi.URLParam(r, "id")

View File

@@ -16,7 +16,7 @@ func NewViewHandler() *ViewHandler {
}
func (h *ViewHandler) GetViewInit(w http.ResponseWriter, r *http.Request) {
db := database.GetDB()
db := database.GetDB().WithContext(r.Context())
userID := middleware.UserIDFromContext(r.Context())
var user models.User

View File

@@ -1,6 +1,7 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"io"
@@ -69,18 +70,18 @@ func (h *WebhookHandler) HandleStripe(w http.ResponseWriter, r *http.Request) {
switch event.Type {
case "checkout.session.completed":
h.handleCheckoutSessionCompleted(event)
h.handleCheckoutSessionCompleted(r.Context(), event)
// payment_intent.succeeded fires when a charge completes asynchronously
// (e.g. 3DS, bank redirects). For confirm=true intents this is normally
// already handled by ConvertCart, but the webhook is the authoritative source.
case "payment_intent.succeeded":
h.handlePaymentIntentSucceeded(event)
h.handlePaymentIntentSucceeded(r.Context(), event)
// payment_intent.payment_failed fires when a payment fails after initial
// confirmation (e.g. bank decline on a deferred 3DS flow).
case "payment_intent.payment_failed":
h.handlePaymentIntentFailed(event)
h.handlePaymentIntentFailed(r.Context(), event)
// Sync local card state when Stripe's side changes.
case "payment_method.attached",
@@ -94,7 +95,7 @@ func (h *WebhookHandler) HandleStripe(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func (h *WebhookHandler) handlePaymentIntentSucceeded(event stripe.Event) {
func (h *WebhookHandler) handlePaymentIntentSucceeded(ctx context.Context, event stripe.Event) {
var pi stripe.PaymentIntent
if err := json.Unmarshal(event.Data.Raw, &pi); err != nil {
webhookLog().Error("failed to unmarshal payment_intent", "error", err, "event", "payment_intent.succeeded")
@@ -107,7 +108,7 @@ func (h *WebhookHandler) handlePaymentIntentSucceeded(event stripe.Event) {
return
}
db := database.GetDB()
db := database.GetDB().WithContext(ctx)
var order models.Order
if err := db.Where("id = ?", orderIDStr).First(&order).Error; err != nil {
webhookLog().Warn("order not found for payment intent", "order_id", orderIDStr, "pi", pi.ID)
@@ -149,7 +150,7 @@ func (h *WebhookHandler) handlePaymentIntentSucceeded(event stripe.Event) {
webhookLog().Info("order marked paid", "order_id", orderIDStr, "pi", pi.ID)
}
func (h *WebhookHandler) handlePaymentIntentFailed(event stripe.Event) {
func (h *WebhookHandler) handlePaymentIntentFailed(ctx context.Context, event stripe.Event) {
var pi stripe.PaymentIntent
if err := json.Unmarshal(event.Data.Raw, &pi); err != nil {
webhookLog().Error("failed to unmarshal payment_intent", "error", err, "event", "payment_intent.payment_failed")
@@ -161,7 +162,7 @@ func (h *WebhookHandler) handlePaymentIntentFailed(event stripe.Event) {
return
}
db := database.GetDB()
db := database.GetDB().WithContext(ctx)
if err := db.Model(&models.Order{}).Where("id = ? AND status = ?", orderIDStr, models.OrderStatusPending).
Update("status", models.OrderStatusFailed).Error; err != nil {
webhookLog().Error("failed to mark order failed", "order_id", orderIDStr, "error", err)
@@ -174,7 +175,7 @@ func (h *WebhookHandler) handlePaymentIntentFailed(event stripe.Event) {
webhookLog().Warn("payment failed", "order_id", orderIDStr, "pi", pi.ID, "reason", errMsg)
}
func (h *WebhookHandler) handleCheckoutSessionCompleted(event stripe.Event) {
func (h *WebhookHandler) handleCheckoutSessionCompleted(ctx context.Context, event stripe.Event) {
var sess stripe.CheckoutSession
if err := json.Unmarshal(event.Data.Raw, &sess); err != nil {
webhookLog().Error("failed to unmarshal checkout session", "error", err)
@@ -209,7 +210,7 @@ func (h *WebhookHandler) handleCheckoutSessionCompleted(event stripe.Event) {
return
}
db := database.GetDB()
db := database.GetDB().WithContext(ctx)
var user models.User
if err := db.Where("stripe_customer_id = ?", sess.Customer.ID).First(&user).Error; err != nil {
webhookLog().Warn("user not found for customer", "customer", sess.Customer.ID, "error", err)

View File

@@ -17,6 +17,7 @@ import (
"terminalShop/pkg/config"
"terminalShop/pkg/database"
"terminalShop/pkg/observability"
"terminalShop/pkg/stripeclient"
)
const version = "v0.1.0"
@@ -61,6 +62,8 @@ func main() {
defer shutdown(context.Background())
}
stripeclient.InitOTel()
// Init JWT manager (same secret + duration as SSH server)
jwtManager := auth.NewJWTManager(cfg.JWTSecret, 30*time.Minute)