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,89 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/billing"
|
||||
)
|
||||
|
||||
// writePlanGate writes a 402 plan_gate payload when err is a known billing gate.
|
||||
// Returns true when the response was written.
|
||||
func writePlanGate(w http.ResponseWriter, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if !(errors.Is(err, billing.ErrInsufficientCredits) ||
|
||||
errors.Is(err, billing.ErrProductLimitExceeded) ||
|
||||
errors.Is(err, billing.ErrAIRequiresUpgrade) ||
|
||||
errors.Is(err, billing.ErrEPRELRequiresUpgrade) ||
|
||||
errors.Is(err, billing.ErrFeatureDisabled)) {
|
||||
return false
|
||||
}
|
||||
body := map[string]any{
|
||||
"error": err.Error(),
|
||||
"code": planGateCode(err),
|
||||
"upgrade_url": "/pricing",
|
||||
}
|
||||
if errors.Is(err, billing.ErrFeatureDisabled) {
|
||||
body["error"] = "feature_disabled"
|
||||
if key := billing.FeatureKeyFromError(err); key != "" {
|
||||
body["feature"] = key
|
||||
}
|
||||
}
|
||||
JSON(w, http.StatusPaymentRequired, body)
|
||||
return true
|
||||
}
|
||||
|
||||
// requireFeatures rejects with 402 when any key is not effective for the company.
|
||||
// Billing nil: pass-through only outside production; production fails closed with 503.
|
||||
// Returns false when the response was already written.
|
||||
func (s *Server) requireFeatures(w http.ResponseWriter, r *http.Request, keys ...string) bool {
|
||||
if len(keys) == 0 {
|
||||
return true
|
||||
}
|
||||
if s.testAssertFeatures != nil {
|
||||
if err := s.testAssertFeatures(r.Context(), keys...); err != nil {
|
||||
if writePlanGate(w, err) {
|
||||
return false
|
||||
}
|
||||
Error(w, http.StatusInternalServerError, "feature check failed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if s.Billing == nil {
|
||||
if s.Config.IsProduction() {
|
||||
Error(w, http.StatusServiceUnavailable, "billing unavailable")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
cid, ok := CompanyIDFromContext(r.Context())
|
||||
if !ok {
|
||||
Error(w, http.StatusUnauthorized, "company required")
|
||||
return false
|
||||
}
|
||||
if err := s.Billing.AssertFeatures(r.Context(), cid, keys...); err != nil {
|
||||
if writePlanGate(w, err) {
|
||||
return false
|
||||
}
|
||||
Error(w, http.StatusInternalServerError, "feature check failed")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RequireFeature rejects the request with 402 when the company's effective
|
||||
// features do not include key. Billing nil: pass-through only outside production;
|
||||
// production fails closed with 503 (never silently allow all features).
|
||||
func (s *Server) RequireFeature(key string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.requireFeatures(w, r, key) {
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user