Files
Crussell/backend/handlers/webhooks/square.go
T
popertotsandSisyphus e4b9003439 refactor(handlers): migrate remaining backend handlers to clock.Now() and transaction patterns
Apply clock.Now() migration, transaction wrapping, and minor refactors across admin, scheduling, today, user, auth handler, notifications, webhooks, services, portfolio, ratelimit, testutils, and main.go.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-24 23:43:50 +01:00

95 lines
2.8 KiB
Go

package webhooks
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"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()
// Verification logic per Square spec (HMAC-SHA256, base64, notificationURL + body).
// Production setup: set SQUARE_WEBHOOK_SIGNATURE_KEY and SQUARE_WEBHOOK_NOTIFICATION_URL
// in env vars (see Square Developer Console → Webhooks → Subscription).
// Reference: https://developer.squareup.com/docs/webhooks/step3validate
signingKey := os.Getenv("SQUARE_WEBHOOK_SIGNATURE_KEY")
notificationURL := os.Getenv("SQUARE_WEBHOOK_NOTIFICATION_URL")
if notificationURL == "" {
notificationURL = "http://localhost:8080/webhooks/square"
}
if signingKey != "" {
signature := r.Header.Get("x-square-hmacsha256-signature")
if signature == "" {
log.Printf("Missing Square webhook signature header")
http.Error(w, "Invalid signature", http.StatusForbidden)
return
}
if !verifySquareSignature(body, signature, signingKey, notificationURL) {
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, notificationURL string) bool {
mac := hmac.New(sha256.New, []byte(signingKey))
mac.Write([]byte(notificationURL))
mac.Write(body)
expected := base64.StdEncoding.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))
}