三个问题:
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(自托管放行)。
136 lines
3.7 KiB
Go
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
|
|
}
|
|
}
|