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>
112 lines
3.7 KiB
Go
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))
|
|
}
|