Files
Crussell/backend/handlers/webhooks/square.go
T
popertotsandSisyphus 3d0e2afc4c refactor(backend): migrate db.DB to db.Conn PoolProxy across all handlers
Replace direct *pgxpool.Pool usage with PoolProxy wrapper across the entire backend:

- db.DB renamed to db.Conn (*pgxpool.Pool -> *PoolProxy)
- JWT functions now accept context.Context instead of using context.Background()
- Handler DB calls route through PoolProxy for per-test transaction support
- Fixture/helper/testdb functions accept Querier interface for decoupling
- Query ordering fixed in bookings handlers: COUNT after data query to avoid pgx conn busy
- Time truncation fixed: time.Date instead of Truncate(24*time.Hour) for week start calc
- testmain_test.go files updated with SeedBaseline and NewPoolProxy

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-21 19:28:54 +01:00

112 lines
3.7 KiB
Go

package webhooks
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
)
type SquareWebhookEvent struct {
Type string `json:"type"`
EventID string `json:"event_id"`
CreatedAt string `json:"created_at"`
Data json.RawMessage `json:"data"`
LocationID string `json:"location_id"`
}
func HandleSquareWebhook(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 512*1024)
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("Failed to read webhook body: %v", err)
http.Error(w, "request body too large or unreadable", http.StatusRequestEntityTooLarge)
return
}
defer r.Body.Close()
// TODO(PROD): Replace this dev stub with production webhook verification.
//
// Square webhook verification requirements (from official docs):
// 1. Header: `x-square-hmacsha256-signature` (NOT x-square-signature)
// 2. Algorithm: HMAC-SHA256, output is **base64** encoded (not hex)
// 3. Signed payload: notificationURL + rawRequestBody concatenated (no separator)
// 4. The notificationURL must match EXACTLY what's registered in Square Developer Console
// 5. Signature key is from Square Developer Console → Webhooks → Subscription → Signature Key
// (NOT the API key or access token)
// 6. ALWAYS verify — reject with 403 if missing/invalid
// 7. Use timing-safe comparison (hmac.Equal)
//
// Reference: https://developer.squareup.com/docs/webhooks/step3validate
//
// For the Go SDK approach:
// import "github.com/square/square-go-sdk"
// client := square.NewClient()
// err := client.Webhooks.VerifySignature(ctx, &square.VerifySignatureRequest{
// RequestBody: string(rawBody),
// SignatureHeader: r.Header.Get("x-square-hmacsha256-signature"),
// SignatureKey: os.Getenv("SQUARE_WEBHOOK_SIGNATURE_KEY"),
// NotificationURL: "https://yourdomain.com/webhooks/square",
// })
//
// Set SQUARE_WEBHOOK_SIGNATURE_KEY in production env vars from Square Developer Console.
// Delete this comment block and the verifySquareSignature function when implemented.
signingKey := os.Getenv("SQUARE_WEBHOOK_SIGNATURE_KEY")
if signingKey != "" {
signature := r.Header.Get("x-square-signature")
if signature == "" {
log.Printf("Missing Square webhook signature header")
http.Error(w, "Invalid signature", http.StatusForbidden)
return
}
if !verifySquareSignature(body, signature, signingKey) {
log.Printf("Invalid Square webhook signature")
http.Error(w, "Invalid signature", http.StatusForbidden)
return
}
}
var event SquareWebhookEvent
if err := json.Unmarshal(body, &event); err != nil {
log.Printf("Failed to parse webhook event: %v", err)
http.Error(w, "Invalid event", http.StatusBadRequest)
return
}
log.Printf("[SQUARE-WEBHOOK] Received event: %s", event.Type)
switch event.Type {
case "payment.updated":
handlePaymentUpdated(event.Data)
case "refund.updated":
handleRefundUpdated(event.Data)
case "dispute.created":
log.Printf("[SQUARE-WEBHOOK] Dispute created: %s", event.EventID)
default:
log.Printf("[SQUARE-WEBHOOK] Unknown event type: %s", event.Type)
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
func verifySquareSignature(body []byte, signature, signingKey string) bool {
mac := hmac.New(sha256.New, []byte(signingKey))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
func handlePaymentUpdated(data json.RawMessage) {
log.Printf("[SQUARE-WEBHOOK] payment.updated: %s", string(data))
}
func handleRefundUpdated(data json.RawMessage) {
log.Printf("[SQUARE-WEBHOOK] refund.updated: %s", string(data))
}