package handlers import ( "encoding/json" "io" "log" "net/http" stripego "github.com/stripe/stripe-go/v84" "github.com/stripe/stripe-go/v84/webhook" ) const maxWebhookPayload = 65536 func (a *App) handleWebhook(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(io.LimitReader(r.Body, maxWebhookPayload)) if err != nil { http.Error(w, "read error", http.StatusBadRequest) return } sig := r.Header.Get("Stripe-Signature") event, err := webhook.ConstructEvent(body, sig, a.stripe.WebhookSecret()) if err != nil { log.Printf("webhook signature verification failed: %v", err) http.Error(w, "invalid signature", http.StatusUnauthorized) return } switch event.Type { case "checkout.session.completed": a.onCheckoutCompleted(event) case "customer.subscription.deleted": a.onSubscriptionDeleted(event) case "customer.subscription.updated": a.onSubscriptionUpdated(event) default: log.Printf("unhandled webhook event: %s", event.Type) } w.WriteHeader(http.StatusOK) } // Reconciliation backstop: ensures LLDAP user + Stripe ID are set. // Does NOT send password reset — that's the success page's responsibility // so it can reliably show the welcome/onboarding page. func (a *App) onCheckoutCompleted(event stripego.Event) { var sess stripego.CheckoutSession if err := json.Unmarshal(event.Data.Raw, &sess); err != nil { log.Printf("checkout unmarshal error: %v", err) return } email := sess.CustomerDetails.Email customerID := sess.Customer.ID username := sanitizeUsername(email) log.Printf("webhook: checkout completed email=%s customer=%s", email, customerID) if err := a.ldap.EnsureUser(username, email, customerID); err != nil { log.Printf("webhook: ldap ensure user failed: %v", err) } } func (a *App) onSubscriptionDeleted(event stripego.Event) { var sub stripego.Subscription if err := json.Unmarshal(event.Data.Raw, &sub); err != nil { log.Printf("subscription unmarshal error: %v", err) return } customerID := sub.Customer.ID log.Printf("subscription deleted for customer %s", customerID) username, err := a.ldap.FindUserByStripeID(customerID) if err != nil { log.Printf("could not find user for customer %s: %v", customerID, err) return } if err := a.ldap.RemoveFromGroup(username, "customers"); err != nil { log.Printf("ldap group remove failed: %v", err) } stackName := "customer-" + username if err := a.swarm.RemoveStack(stackName); err != nil { log.Printf("stack remove failed for %s: %v", stackName, err) } log.Printf("deprovisioned stack for customer %s (%s)", customerID, username) } func (a *App) onSubscriptionUpdated(event stripego.Event) { var sub stripego.Subscription if err := json.Unmarshal(event.Data.Raw, &sub); err != nil { log.Printf("subscription update unmarshal error: %v", err) return } if sub.Status == stripego.SubscriptionStatusCanceled || sub.Status == stripego.SubscriptionStatusUnpaid { log.Printf("subscription %s status=%s, will be cleaned up on deletion", sub.ID, sub.Status) } }