forked from Nixius/authelia
159 lines
4.7 KiB
Go
159 lines
4.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"git.nixc.us/a250/ss-atlas/internal/accounts"
|
|
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)
|
|
case "invoice.paid":
|
|
a.onInvoicePaid(event)
|
|
default:
|
|
log.Printf("unhandled webhook event: %s", event.Type)
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// Reconciliation backstop: ensures the account, billing link, and instance exist.
|
|
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
|
|
phone := ""
|
|
domain := ""
|
|
accountID := int64(0)
|
|
if sess.Metadata != nil {
|
|
phone = sess.Metadata["customer_phone"]
|
|
domain = sess.Metadata["customer_domain"]
|
|
accountID, _ = strconv.ParseInt(sess.Metadata["account_id"], 10, 64)
|
|
}
|
|
|
|
log.Printf("webhook: checkout completed email=%s customer=%s account_id=%d", email, customerID, accountID)
|
|
|
|
if a.cfg.MaxSignups > 0 {
|
|
count, err := a.accounts.CountCustomers(context.Background())
|
|
if err == nil && count >= a.cfg.MaxSignups {
|
|
log.Printf("webhook: signup limit reached (%d), skipping provision for %s", a.cfg.MaxSignups, email)
|
|
return
|
|
}
|
|
}
|
|
|
|
_, _, err := a.accounts.UpsertCheckout(context.Background(), accounts.CheckoutInput{
|
|
AccountID: accountID,
|
|
Email: email,
|
|
DisplayName: email,
|
|
Phone: phone,
|
|
CustomerDomain: domain,
|
|
StripeCustomerID: customerID,
|
|
StripeSubscriptionID: subscriptionIDFromSession(&sess),
|
|
StripeSessionID: sess.ID,
|
|
StripeEventID: event.ID,
|
|
})
|
|
if err != nil {
|
|
log.Printf("webhook: account ensure failed: %v", err)
|
|
}
|
|
|
|
subID := ""
|
|
if sess.Subscription != nil && sess.Subscription.ID != "" {
|
|
subID = sess.Subscription.ID
|
|
} else {
|
|
var raw struct {
|
|
Subscription interface{} `json:"subscription"`
|
|
}
|
|
if json.Unmarshal(event.Data.Raw, &raw) == nil {
|
|
if s, ok := raw.Subscription.(string); ok {
|
|
subID = s
|
|
}
|
|
}
|
|
}
|
|
if subID != "" {
|
|
a.maybeScheduleFreeTierCancel(subID)
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
_, inst, err := a.accounts.AccountByStripeCustomerID(context.Background(), customerID)
|
|
if err != nil {
|
|
log.Printf("could not find account for customer %s: %v", customerID, err)
|
|
return
|
|
}
|
|
|
|
if err := a.accounts.MarkSubscriptionStatus(context.Background(), customerID, "cancelled"); err != nil && !errors.Is(err, accounts.ErrNotFound) {
|
|
log.Printf("subscription status update failed: %v", err)
|
|
}
|
|
|
|
if err := a.swarm.ArchiveVolumes(inst.StackName, a.cfg.ArchivePath); err != nil {
|
|
log.Printf("archive failed for %s: %v", inst.StackName, err)
|
|
}
|
|
|
|
if err := a.swarm.RemoveStack(inst.StackName); err != nil {
|
|
log.Printf("stack remove failed for %s: %v", inst.StackName, err)
|
|
}
|
|
_ = a.accounts.UpdateInstanceState(context.Background(), inst.StackName, "archived", false)
|
|
|
|
log.Printf("deprovisioned stack for customer %s (%s), volumes archived", customerID, inst.Slug)
|
|
}
|
|
|
|
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 {
|
|
if sub.Customer != nil && sub.Customer.ID != "" {
|
|
_ = a.accounts.MarkSubscriptionStatus(context.Background(), sub.Customer.ID, string(sub.Status))
|
|
}
|
|
log.Printf("subscription %s status=%s, will be cleaned up on deletion", sub.ID, sub.Status)
|
|
}
|
|
}
|