Files
descrybe/apps/api/internal/httpapi/stripe_handlers.go
T

116 lines
3.7 KiB
Go
Raw Normal View History

package httpapi
import (
"errors"
"io"
"net/http"
"github.com/descrybe/descrybe-v2/apps/api/internal/billing"
)
func (s *Server) stripeSvc() *billing.StripeService {
if s.Stripe != nil {
return s.Stripe
}
s.Stripe = &billing.StripeService{
Pool: s.Pool,
Billing: s.Billing,
Cfg: billing.StripeConfig{
SecretKey: s.Config.StripeSecretKey,
WebhookSecret: s.Config.StripeWebhookSecret,
WebOrigin: s.Config.WebOrigin,
PublicAPIURL: s.Config.PublicAPIURL,
PriceIDs: s.Config.StripePriceIDs,
ForceMock: s.Config.StripeMock,
},
}
return s.Stripe
}
func (s *Server) handleStripeStatus(w http.ResponseWriter, r *http.Request) {
cid, _ := CompanyIDFromContext(r.Context())
st, err := s.stripeSvc().Status(r.Context(), cid)
if err != nil {
Error(w, http.StatusInternalServerError, "failed to load stripe status")
return
}
JSON(w, http.StatusOK, st)
}
func (s *Server) handleStripeCheckout(w http.ResponseWriter, r *http.Request) {
cid, _ := CompanyIDFromContext(r.Context())
uid, _ := UserIDFromContext(r.Context())
role, _ := RoleFromContext(r.Context())
if role != "admin" {
Error(w, http.StatusForbidden, "admin required")
return
}
2026-08-16 11:37:42 +02:00
platformAdmin, err := s.checkPlatformAdmin(r.Context(), uid)
if err != nil {
LogAndError(w, http.StatusInternalServerError, "failed to verify platform admin", err)
return
}
if err := s.stripeSvc().EnsureSelfServeCheckout(r.Context(), platformAdmin); err != nil {
ClientOrLog(w, http.StatusServiceUnavailable, "checkout unavailable", err, billing.ClientError)
return
}
var body billing.CheckoutRequest
if err := DecodeJSON(r, &body); err != nil {
Error(w, http.StatusBadRequest, "invalid json")
return
}
email, name := "", ""
_ = s.Pool.QueryRow(r.Context(), `SELECT email, COALESCE(name, '') FROM users WHERE id = $1`, uid).Scan(&email, &name)
var companyName string
_ = s.Pool.QueryRow(r.Context(), `SELECT name FROM companies WHERE id = $1`, cid).Scan(&companyName)
res, err := s.stripeSvc().CreateCheckoutSession(r.Context(), cid, email, companyName, body)
if err != nil {
ClientOrLog(w, http.StatusBadRequest, "checkout failed", err, billing.ClientError)
return
}
JSON(w, http.StatusOK, res)
}
func (s *Server) handleStripePortal(w http.ResponseWriter, r *http.Request) {
cid, _ := CompanyIDFromContext(r.Context())
role, _ := RoleFromContext(r.Context())
if role != "admin" {
Error(w, http.StatusForbidden, "admin required")
return
}
res, err := s.stripeSvc().CreatePortalSession(r.Context(), cid)
if err != nil {
ClientOrLog(w, http.StatusBadRequest, "portal session failed", err, billing.ClientError)
return
}
JSON(w, http.StatusOK, res)
}
// handleStripeWebhook is public (no session/CSRF). Signature verified when STRIPE_WEBHOOK_SECRET is set.
func (s *Server) handleStripeWebhook(w http.ResponseWriter, r *http.Request) {
const maxBody = 1 << 20 // 1 MiB
body, err := io.ReadAll(io.LimitReader(r.Body, maxBody+1))
if err != nil {
Error(w, http.StatusBadRequest, "failed to read body")
return
}
if len(body) > maxBody {
Error(w, http.StatusRequestEntityTooLarge, "body too large")
return
}
sig := r.Header.Get("Stripe-Signature")
if err := s.stripeSvc().HandleWebhook(r.Context(), body, sig); err != nil {
switch {
case errors.Is(err, billing.ErrStripeBadSignature):
Error(w, http.StatusBadRequest, "invalid signature")
case errors.Is(err, billing.ErrStripeNotConfigured):
Error(w, http.StatusServiceUnavailable, "stripe webhooks not configured")
default:
// Avoid leaking internal apply/DB details to an unauthenticated caller.
LogAndError(w, http.StatusBadRequest, "webhook processing failed", err)
}
return
}
JSON(w, http.StatusOK, map[string]string{"status": "ok"})
}