Files
gochat/backend/internal/middleware/feature_flag.go
T
Rogee 37e6d77a2d fix: 帮助中心设置页报 Feature 'knowledge_base' is not available
三个问题:

1. router.go: portal 路由检查 FeatureKnowledgeBase("knowledge_base"),
   但账户 feature_flags 中只有 help_center,没有 knowledge_base。
   改为检查 FeatureHelpCenter("help_center")。

2. feature_flag.go: 中间件从 context 读取 feature_flags,但没有任何
   上游中间件设置它,导致 exists=false 直接 403。GoChat 是自托管非 SaaS,
   无 flags 时应放行(所有功能可用)。

3. feature_flag.go: Account.FeatureFlags 存储为 JSON 对象格式
   ({"help_center":true}),但旧代码只解析 JSON 数组格式。
   新增 JSON 对象解析支持。

测试:新增 JSONObjectFormat / JSONObjectFormatDisabled 用例,
更新 NoFeatureFlags 用例为 200(自托管放行)。
2026-07-30 11:40:02 +08:00

136 lines
3.7 KiB
Go

package middleware
// Reference: P2E §2.8 — Feature flag middleware
// Feature gating middleware. In GoChat (self-hosted), all features are available
// by default. This middleware exists for API compatibility with Chatwoot's
// enterprise feature gating, but defaults to allow when no flags are configured.
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/pkg/response"
)
// Known feature flags from Chatwoot enterprise features:
// Reference: Chatwoot app/models/account.rb — FEATURE_FLAGS constant
const (
FeatureAutoAssignment = "auto_assignment"
FeatureCustomRoles = "custom_roles"
FeatureAutomation = "automation"
FeatureKnowledgeBase = "knowledge_base"
FeatureReporting = "reporting"
FeatureCSAT = "csat"
FeatureHelpCenter = "help_center"
FeatureAgentBots = "agent_bots"
FeatureMFA = "mfa"
FeatureChannelFacebook = "channel_facebook"
FeatureChannelTwilio = "channel_twilio"
FeatureChannelWhatsApp = "channel_whatsapp"
FeatureChannelEmail = "channel_email"
FeatureChannelTelegram = "channel_telegram"
FeatureChannelLine = "channel_line"
FeatureEnterpriseReporting = "enterprise_reporting"
FeatureMacro = "macro"
FeatureCampaign = "campaign"
FeatureTeamInboxes = "team_inboxes"
)
// FeatureFlagCheck creates a middleware that verifies the current account
// has the specified feature flag enabled.
//
// In GoChat (self-hosted), all features are available by default.
// If feature flags are present in the context, they are checked.
// If no flags are present, the request is allowed (self-hosted = all features enabled).
func FeatureFlagCheck(flag string) gin.HandlerFunc {
return func(c *gin.Context) {
featureFlags, exists := c.Get("feature_flags")
if !exists {
// Try to load from account if it was set
accountFlags, accExists := c.Get("account_feature_flags")
if accExists {
featureFlags = accountFlags
exists = true
}
}
if !exists {
// No feature flags in context — allow in self-hosted mode.
// GoChat is not a SaaS; all features are available.
c.Next()
return
}
if hasFeatureFlag(featureFlags, flag) {
c.Next()
return
}
response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden,
"Feature '"+flag+"' is not enabled on this account")
}
}
// hasFeatureFlag checks if a flag is enabled in the feature flags.
// Feature flags can be stored as:
// - JSON object: {"help_center": true, "csat": false}
// - JSON array: ["help_center", "csat"]
// - Comma-separated string: "help_center,csat"
// - []string: {"help_center", "csat"}
func hasFeatureFlag(raw interface{}, flag string) bool {
switch v := raw.(type) {
case []string:
for _, f := range v {
if f == flag {
return true
}
}
return false
case string:
if v == "" {
return false
}
// Try JSON object first: {"flag": true}
var obj map[string]interface{}
if err := json.Unmarshal([]byte(v), &obj); err == nil {
if val, ok := obj[flag]; ok {
if b, ok := val.(bool); ok {
return b
}
return true // non-bool value = present = enabled
}
return false
}
// Try JSON array: ["flag"]
var arr []string
if err := json.Unmarshal([]byte(v), &arr); err == nil {
for _, f := range arr {
if f == flag {
return true
}
}
return false
}
return false
case map[string]interface{}:
if val, ok := v[flag]; ok {
if b, ok := val.(bool); ok {
return b
}
return true
}
return false
case []interface{}:
for _, item := range v {
if s, ok := item.(string); ok && s == flag {
return true
}
}
return false
default:
return false
}
}