Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
107 lines
3.3 KiB
Go
107 lines
3.3 KiB
Go
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
|
|
}
|
|
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"})
|
|
}
|