feat(channels): gate available channels behind feature switch (backend)
Add a DB-backed soft switch "available_channels_enabled" controlling the user-facing /channels/available endpoint and sidebar entry. Default to false (opt-in) — the feature stays invisible until an admin enables it under Admin Settings > Features. - domain_constants: SettingKeyAvailableChannelsEnabled - settings_view: AllSettings/PublicSettings + AvailableChannelsEnabled - setting_service: public+all read/write, seed default "false", GetAvailableChannelsRuntime helper (fail-closed on read error) - admin setting_handler: UpdateSettingsRequest *bool + update branch + audit diff entry - public setting_handler: expose via GET /api/v1/settings - available_channel_handler: featureEnabled() guard — returns empty list after auth when disabled (401 precedes the feature check to preserve existing behavior)
This commit is contained in:
@@ -234,7 +234,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService, opsService, paymentConfigService, paymentService)
|
settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService, opsService, paymentConfigService, paymentService)
|
||||||
paymentOrderExpiryService := service.ProvidePaymentOrderExpiryService(paymentService)
|
paymentOrderExpiryService := service.ProvidePaymentOrderExpiryService(paymentService)
|
||||||
paymentHandler := admin.NewPaymentHandler(paymentService, paymentConfigService)
|
paymentHandler := admin.NewPaymentHandler(paymentService, paymentConfigService)
|
||||||
availableChannelUserHandler := handler.NewAvailableChannelHandler(channelService, apiKeyService)
|
availableChannelUserHandler := handler.NewAvailableChannelHandler(channelService, apiKeyService, settingService)
|
||||||
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler, channelMonitorHandler, channelMonitorRequestTemplateHandler, paymentHandler)
|
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler, channelMonitorHandler, channelMonitorRequestTemplateHandler, paymentHandler)
|
||||||
usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig)
|
usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig)
|
||||||
userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient)
|
userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient)
|
||||||
|
|||||||
@@ -238,6 +238,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
|||||||
|
|
||||||
ChannelMonitorEnabled: settings.ChannelMonitorEnabled,
|
ChannelMonitorEnabled: settings.ChannelMonitorEnabled,
|
||||||
ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds,
|
ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds,
|
||||||
|
|
||||||
|
AvailableChannelsEnabled: settings.AvailableChannelsEnabled,
|
||||||
}
|
}
|
||||||
response.Success(c, systemSettingsResponseData(payload, authSourceDefaults))
|
response.Success(c, systemSettingsResponseData(payload, authSourceDefaults))
|
||||||
}
|
}
|
||||||
@@ -432,6 +434,9 @@ type UpdateSettingsRequest struct {
|
|||||||
// Channel Monitor feature switch
|
// Channel Monitor feature switch
|
||||||
ChannelMonitorEnabled *bool `json:"channel_monitor_enabled"`
|
ChannelMonitorEnabled *bool `json:"channel_monitor_enabled"`
|
||||||
ChannelMonitorDefaultIntervalSeconds *int `json:"channel_monitor_default_interval_seconds"`
|
ChannelMonitorDefaultIntervalSeconds *int `json:"channel_monitor_default_interval_seconds"`
|
||||||
|
|
||||||
|
// Available Channels feature switch (user-facing)
|
||||||
|
AvailableChannelsEnabled *bool `json:"available_channels_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSettings 更新系统设置
|
// UpdateSettings 更新系统设置
|
||||||
@@ -1238,6 +1243,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
return previousSettings.ChannelMonitorDefaultIntervalSeconds
|
return previousSettings.ChannelMonitorDefaultIntervalSeconds
|
||||||
}(),
|
}(),
|
||||||
|
AvailableChannelsEnabled: func() bool {
|
||||||
|
if req.AvailableChannelsEnabled != nil {
|
||||||
|
return *req.AvailableChannelsEnabled
|
||||||
|
}
|
||||||
|
return previousSettings.AvailableChannelsEnabled
|
||||||
|
}(),
|
||||||
}
|
}
|
||||||
|
|
||||||
authSourceDefaults := &service.AuthSourceDefaultSettings{
|
authSourceDefaults := &service.AuthSourceDefaultSettings{
|
||||||
@@ -1471,6 +1482,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
|
|
||||||
ChannelMonitorEnabled: updatedSettings.ChannelMonitorEnabled,
|
ChannelMonitorEnabled: updatedSettings.ChannelMonitorEnabled,
|
||||||
ChannelMonitorDefaultIntervalSeconds: updatedSettings.ChannelMonitorDefaultIntervalSeconds,
|
ChannelMonitorDefaultIntervalSeconds: updatedSettings.ChannelMonitorDefaultIntervalSeconds,
|
||||||
|
|
||||||
|
AvailableChannelsEnabled: updatedSettings.AvailableChannelsEnabled,
|
||||||
}
|
}
|
||||||
response.Success(c, systemSettingsResponseData(payload, updatedAuthSourceDefaults))
|
response.Success(c, systemSettingsResponseData(payload, updatedAuthSourceDefaults))
|
||||||
}
|
}
|
||||||
@@ -1833,6 +1846,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
|||||||
if before.ChannelMonitorDefaultIntervalSeconds != after.ChannelMonitorDefaultIntervalSeconds {
|
if before.ChannelMonitorDefaultIntervalSeconds != after.ChannelMonitorDefaultIntervalSeconds {
|
||||||
changed = append(changed, "channel_monitor_default_interval_seconds")
|
changed = append(changed, "channel_monitor_default_interval_seconds")
|
||||||
}
|
}
|
||||||
|
if before.AvailableChannelsEnabled != after.AvailableChannelsEnabled {
|
||||||
|
changed = append(changed, "available_channels_enabled")
|
||||||
|
}
|
||||||
changed = appendAuthSourceDefaultChanges(changed, beforeAuthSourceDefaults, afterAuthSourceDefaults)
|
changed = appendAuthSourceDefaultChanges(changed, beforeAuthSourceDefaults, afterAuthSourceDefaults)
|
||||||
return changed
|
return changed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,19 +21,30 @@ import (
|
|||||||
type AvailableChannelHandler struct {
|
type AvailableChannelHandler struct {
|
||||||
channelService *service.ChannelService
|
channelService *service.ChannelService
|
||||||
apiKeyService *service.APIKeyService
|
apiKeyService *service.APIKeyService
|
||||||
|
settingService *service.SettingService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAvailableChannelHandler 创建用户侧可用渠道 handler。
|
// NewAvailableChannelHandler 创建用户侧可用渠道 handler。
|
||||||
func NewAvailableChannelHandler(
|
func NewAvailableChannelHandler(
|
||||||
channelService *service.ChannelService,
|
channelService *service.ChannelService,
|
||||||
apiKeyService *service.APIKeyService,
|
apiKeyService *service.APIKeyService,
|
||||||
|
settingService *service.SettingService,
|
||||||
) *AvailableChannelHandler {
|
) *AvailableChannelHandler {
|
||||||
return &AvailableChannelHandler{
|
return &AvailableChannelHandler{
|
||||||
channelService: channelService,
|
channelService: channelService,
|
||||||
apiKeyService: apiKeyService,
|
apiKeyService: apiKeyService,
|
||||||
|
settingService: settingService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// featureEnabled 返回 available-channels 开关是否启用。默认关闭(opt-in)。
|
||||||
|
func (h *AvailableChannelHandler) featureEnabled(c *gin.Context) bool {
|
||||||
|
if h.settingService == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return h.settingService.GetAvailableChannelsRuntime(c.Request.Context()).Enabled
|
||||||
|
}
|
||||||
|
|
||||||
// userAvailableGroup 用户可见的分组概要(白名单字段)。
|
// userAvailableGroup 用户可见的分组概要(白名单字段)。
|
||||||
type userAvailableGroup struct {
|
type userAvailableGroup struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
@@ -89,6 +100,13 @@ func (h *AvailableChannelHandler) List(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Feature 未启用时返回空数组(不暴露渠道信息)。检查放在认证之后,
|
||||||
|
// 保持与未开关前的 401 行为一致:未登录先 401,登录后再按开关决定。
|
||||||
|
if !h.featureEnabled(c) {
|
||||||
|
response.Success(c, []userAvailableChannel{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
userGroups, err := h.apiKeyService.GetAvailableGroups(c.Request.Context(), subject.UserID)
|
userGroups, err := h.apiKeyService.GetAvailableGroups(c.Request.Context(), subject.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.ErrorFrom(c, err)
|
response.ErrorFrom(c, err)
|
||||||
|
|||||||
@@ -187,6 +187,9 @@ type SystemSettings struct {
|
|||||||
// Channel Monitor feature switch
|
// Channel Monitor feature switch
|
||||||
ChannelMonitorEnabled bool `json:"channel_monitor_enabled"`
|
ChannelMonitorEnabled bool `json:"channel_monitor_enabled"`
|
||||||
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
|
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
|
||||||
|
|
||||||
|
// Available Channels feature switch (user-facing aggregate view)
|
||||||
|
AvailableChannelsEnabled bool `json:"available_channels_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefaultSubscriptionSetting struct {
|
type DefaultSubscriptionSetting struct {
|
||||||
@@ -237,6 +240,8 @@ type PublicSettings struct {
|
|||||||
|
|
||||||
ChannelMonitorEnabled bool `json:"channel_monitor_enabled"`
|
ChannelMonitorEnabled bool `json:"channel_monitor_enabled"`
|
||||||
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
|
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
|
||||||
|
|
||||||
|
AvailableChannelsEnabled bool `json:"available_channels_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OverloadCooldownSettings 529过载冷却配置 DTO
|
// OverloadCooldownSettings 529过载冷却配置 DTO
|
||||||
|
|||||||
@@ -73,5 +73,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
|
|||||||
|
|
||||||
ChannelMonitorEnabled: settings.ChannelMonitorEnabled,
|
ChannelMonitorEnabled: settings.ChannelMonitorEnabled,
|
||||||
ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds,
|
ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds,
|
||||||
|
|
||||||
|
AvailableChannelsEnabled: settings.AvailableChannelsEnabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,6 +254,11 @@ const (
|
|||||||
// pre-filled when creating a new channel monitor from the admin UI. Range: [15, 3600].
|
// pre-filled when creating a new channel monitor from the admin UI. Range: [15, 3600].
|
||||||
SettingKeyChannelMonitorDefaultIntervalSeconds = "channel_monitor_default_interval_seconds"
|
SettingKeyChannelMonitorDefaultIntervalSeconds = "channel_monitor_default_interval_seconds"
|
||||||
|
|
||||||
|
// SettingKeyAvailableChannelsEnabled is a DB-backed soft switch for the "Available Channels"
|
||||||
|
// user-facing aggregate view. When false: user endpoint returns an empty list and the
|
||||||
|
// sidebar entry is hidden. Defaults to false (opt-in feature).
|
||||||
|
SettingKeyAvailableChannelsEnabled = "available_channels_enabled"
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// Overload Cooldown (529)
|
// Overload Cooldown (529)
|
||||||
// =========================
|
// =========================
|
||||||
|
|||||||
@@ -452,6 +452,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
|||||||
SettingKeyAccountQuotaNotifyEnabled,
|
SettingKeyAccountQuotaNotifyEnabled,
|
||||||
SettingKeyChannelMonitorEnabled,
|
SettingKeyChannelMonitorEnabled,
|
||||||
SettingKeyChannelMonitorDefaultIntervalSeconds,
|
SettingKeyChannelMonitorDefaultIntervalSeconds,
|
||||||
|
SettingKeyAvailableChannelsEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
||||||
@@ -537,6 +538,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
|||||||
|
|
||||||
ChannelMonitorEnabled: !isFalseSettingValue(settings[SettingKeyChannelMonitorEnabled]),
|
ChannelMonitorEnabled: !isFalseSettingValue(settings[SettingKeyChannelMonitorEnabled]),
|
||||||
ChannelMonitorDefaultIntervalSeconds: parseChannelMonitorInterval(settings[SettingKeyChannelMonitorDefaultIntervalSeconds]),
|
ChannelMonitorDefaultIntervalSeconds: parseChannelMonitorInterval(settings[SettingKeyChannelMonitorDefaultIntervalSeconds]),
|
||||||
|
|
||||||
|
AvailableChannelsEnabled: settings[SettingKeyAvailableChannelsEnabled] == "true",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,6 +598,25 @@ func (s *SettingService) GetChannelMonitorRuntime(ctx context.Context) ChannelMo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AvailableChannelsRuntime is the lightweight view of the available-channels feature
|
||||||
|
// switch consumed by the user-facing handler.
|
||||||
|
type AvailableChannelsRuntime struct {
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAvailableChannelsRuntime reads the available-channels feature switch directly
|
||||||
|
// from the settings store. Fail-closed: on error returns Enabled=false, matching
|
||||||
|
// the opt-in default (unknown ↔ disabled).
|
||||||
|
func (s *SettingService) GetAvailableChannelsRuntime(ctx context.Context) AvailableChannelsRuntime {
|
||||||
|
vals, err := s.settingRepo.GetMultiple(ctx, []string{SettingKeyAvailableChannelsEnabled})
|
||||||
|
if err != nil {
|
||||||
|
return AvailableChannelsRuntime{Enabled: false}
|
||||||
|
}
|
||||||
|
return AvailableChannelsRuntime{
|
||||||
|
Enabled: vals[SettingKeyAvailableChannelsEnabled] == "true",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SetOnUpdateCallback sets a callback function to be called when settings are updated
|
// SetOnUpdateCallback sets a callback function to be called when settings are updated
|
||||||
// This is used for cache invalidation (e.g., HTML cache in frontend server)
|
// This is used for cache invalidation (e.g., HTML cache in frontend server)
|
||||||
func (s *SettingService) SetOnUpdateCallback(callback func()) {
|
func (s *SettingService) SetOnUpdateCallback(callback func()) {
|
||||||
@@ -1151,6 +1173,9 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
|
|||||||
updates[SettingKeyChannelMonitorDefaultIntervalSeconds] = strconv.Itoa(v)
|
updates[SettingKeyChannelMonitorDefaultIntervalSeconds] = strconv.Itoa(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Available channels feature switch
|
||||||
|
updates[SettingKeyAvailableChannelsEnabled] = strconv.FormatBool(settings.AvailableChannelsEnabled)
|
||||||
|
|
||||||
// Claude Code version check
|
// Claude Code version check
|
||||||
updates[SettingKeyMinClaudeCodeVersion] = settings.MinClaudeCodeVersion
|
updates[SettingKeyMinClaudeCodeVersion] = settings.MinClaudeCodeVersion
|
||||||
updates[SettingKeyMaxClaudeCodeVersion] = settings.MaxClaudeCodeVersion
|
updates[SettingKeyMaxClaudeCodeVersion] = settings.MaxClaudeCodeVersion
|
||||||
@@ -1700,6 +1725,9 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
|||||||
SettingKeyChannelMonitorEnabled: "true",
|
SettingKeyChannelMonitorEnabled: "true",
|
||||||
SettingKeyChannelMonitorDefaultIntervalSeconds: "60",
|
SettingKeyChannelMonitorDefaultIntervalSeconds: "60",
|
||||||
|
|
||||||
|
// Available channels feature (default disabled; opt-in)
|
||||||
|
SettingKeyAvailableChannelsEnabled: "false",
|
||||||
|
|
||||||
// Claude Code version check (default: empty = disabled)
|
// Claude Code version check (default: empty = disabled)
|
||||||
SettingKeyMinClaudeCodeVersion: "",
|
SettingKeyMinClaudeCodeVersion: "",
|
||||||
SettingKeyMaxClaudeCodeVersion: "",
|
SettingKeyMaxClaudeCodeVersion: "",
|
||||||
@@ -2008,6 +2036,9 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
|||||||
settings[SettingKeyChannelMonitorDefaultIntervalSeconds],
|
settings[SettingKeyChannelMonitorDefaultIntervalSeconds],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Available channels feature (default: disabled; strict true)
|
||||||
|
result.AvailableChannelsEnabled = settings[SettingKeyAvailableChannelsEnabled] == "true"
|
||||||
|
|
||||||
// Claude Code version check
|
// Claude Code version check
|
||||||
result.MinClaudeCodeVersion = settings[SettingKeyMinClaudeCodeVersion]
|
result.MinClaudeCodeVersion = settings[SettingKeyMinClaudeCodeVersion]
|
||||||
result.MaxClaudeCodeVersion = settings[SettingKeyMaxClaudeCodeVersion]
|
result.MaxClaudeCodeVersion = settings[SettingKeyMaxClaudeCodeVersion]
|
||||||
|
|||||||
@@ -129,6 +129,9 @@ type SystemSettings struct {
|
|||||||
ChannelMonitorEnabled bool `json:"channel_monitor_enabled"`
|
ChannelMonitorEnabled bool `json:"channel_monitor_enabled"`
|
||||||
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
|
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
|
||||||
|
|
||||||
|
// Available Channels feature (user-facing aggregate view)
|
||||||
|
AvailableChannelsEnabled bool `json:"available_channels_enabled"`
|
||||||
|
|
||||||
// Claude Code version check
|
// Claude Code version check
|
||||||
MinClaudeCodeVersion string
|
MinClaudeCodeVersion string
|
||||||
MaxClaudeCodeVersion string
|
MaxClaudeCodeVersion string
|
||||||
@@ -217,6 +220,9 @@ type PublicSettings struct {
|
|||||||
// Channel Monitor feature
|
// Channel Monitor feature
|
||||||
ChannelMonitorEnabled bool `json:"channel_monitor_enabled"`
|
ChannelMonitorEnabled bool `json:"channel_monitor_enabled"`
|
||||||
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
|
ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"`
|
||||||
|
|
||||||
|
// Available Channels feature (user-facing aggregate view)
|
||||||
|
AvailableChannelsEnabled bool `json:"available_channels_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WeChatConnectOAuthConfig struct {
|
type WeChatConnectOAuthConfig struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user