Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
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"})
|
||||
}
|
||||
Reference in New Issue
Block a user