package middleware // Reference: P2E ยง2.8 โ€” Feature flag middleware // Enterprise feature gating middleware that checks if the current account // has a specific feature flag enabled. // Corresponds to Chatwoot's enterprise feature gating in controllers. 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. Feature flags are stored as a JSON // array in the Account.FeatureFlags field. // // Usage: // router.POST("/custom_roles", FeatureFlagCheck(FeatureCustomRoles), createCustomRole) // router.GET("/knowledge_base", FeatureFlagCheck(FeatureKnowledgeBase), listKnowledgeBase) 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 available โ€” deny by default for enterprise features response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden, "Feature '"+flag+"' is not available on this account") return } flags, err := parseFeatureFlags(featureFlags) if err != nil { response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "Failed to parse feature flags") return } if !hasFlag(flags, flag) { response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden, "Feature '"+flag+"' is not enabled on this account. Upgrade to enterprise plan to access this feature.") return } c.Next() } } // parseFeatureFlags parses the feature flags from various formats. // Account.FeatureFlags is stored as a JSON string (text column) or raw string. func parseFeatureFlags(raw interface{}) ([]string, error) { switch v := raw.(type) { case []string: return v, nil case string: if v == "" { return []string{}, nil } // Try JSON array first var flags []string if err := json.Unmarshal([]byte(v), &flags); err == nil { return flags, nil } // Try comma-separated format as fallback // Some accounts store flags as "auto_assignment,custom_roles,automation" return splitCommaList(v), nil case []interface{}: result := make([]string, 0, len(v)) for _, item := range v { if s, ok := item.(string); ok { result = append(result, s) } } return result, nil default: return []string{}, nil } } // hasFlag checks if a flag is present in the flag list. func hasFlag(flags []string, flag string) bool { for _, f := range flags { if f == flag { return true } } return false } // splitCommaList splits a comma-separated string into a slice. func splitCommaList(s string) []string { result := []string{} for _, item := range splitByComma(s) { trimmed := trimSpaces(item) if trimmed != "" { result = append(result, trimmed) } } return result } // Simple string utilities to avoid importing strings package overhead func splitByComma(s string) []string { var parts []string current := "" for _, ch := range s { if ch == ',' { parts = append(parts, current) current = "" } else { current += string(ch) } } if current != "" { parts = append(parts, current) } return parts } func trimSpaces(s string) string { start := 0 end := len(s) for start < end && (s[start] == ' ' || s[start] == '\t') { start++ } for end > start && (s[end-1] == ' ' || s[end-1] == '\t') { end-- } return s[start:end] }