mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-01 06:36:38 +08:00
重构获取公共参数
This commit is contained in:
@@ -34,12 +34,14 @@ type CreateSystemConfigRequest struct {
|
||||
Key string `json:"key" binding:"required,max=64"`
|
||||
Value string `json:"value" binding:"required,max=255"`
|
||||
Type string `json:"type" binding:"required,oneof=system business"`
|
||||
Visibility int `json:"visibility" binding:"oneof=0 1"`
|
||||
Description string `json:"description" binding:"max=255"`
|
||||
}
|
||||
|
||||
// UpdateSystemConfigRequest 更新系统配置请求
|
||||
type UpdateSystemConfigRequest struct {
|
||||
Value string `json:"value" binding:"required,max=255"`
|
||||
Visibility *int `json:"visibility" binding:"omitempty,oneof=0 1"`
|
||||
Description string `json:"description" binding:"max=255"`
|
||||
}
|
||||
|
||||
@@ -78,6 +80,7 @@ func CreateSystemConfig(c *gin.Context) {
|
||||
Key: req.Key,
|
||||
Value: req.Value,
|
||||
Type: req.Type,
|
||||
Visibility: req.Visibility,
|
||||
Description: req.Description,
|
||||
}
|
||||
|
||||
@@ -206,6 +209,10 @@ func UpdateSystemConfig(c *gin.Context) {
|
||||
updates := map[string]interface{}{
|
||||
"description": req.Description,
|
||||
}
|
||||
if req.Visibility != nil {
|
||||
updates["visibility"] = *req.Visibility
|
||||
config.Visibility = *req.Visibility
|
||||
}
|
||||
if key != model.ConfigKeySMTPPassword || req.Value != "******" {
|
||||
updates["value"] = req.Value
|
||||
config.Value = req.Value
|
||||
|
||||
@@ -73,6 +73,7 @@ func TestCreateSystemConfig(t *testing.T) {
|
||||
Key: "custom_key",
|
||||
Value: "custom_value",
|
||||
Type: "system",
|
||||
Visibility: model.ConfigVisibilityVisible,
|
||||
Description: "desc",
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
@@ -99,7 +100,10 @@ func TestCreateSystemConfig(t *testing.T) {
|
||||
t.Fatalf("failed to find system config in Redis: %v", err)
|
||||
}
|
||||
if redisConfig.Value != "custom_value" {
|
||||
t.Errorf("expected value 'custom_value', got '%s'", redisConfig.Value)
|
||||
t.Errorf("CreateSystemConfig(custom_key).Value = %q, want %q", redisConfig.Value, "custom_value")
|
||||
}
|
||||
if redisConfig.Visibility != model.ConfigVisibilityVisible {
|
||||
t.Errorf("CreateSystemConfig(custom_key).Visibility = %d, want %d", redisConfig.Visibility, model.ConfigVisibilityVisible)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -217,8 +221,10 @@ func TestUpdateSystemConfig(t *testing.T) {
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
t.Run("update successfully", func(t *testing.T) {
|
||||
hidden := model.ConfigVisibilityHidden
|
||||
payload := UpdateSystemConfigRequest{
|
||||
Value: "Super Site Name",
|
||||
Visibility: &hidden,
|
||||
Description: "Updated Description",
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
@@ -234,7 +240,7 @@ func TestUpdateSystemConfig(t *testing.T) {
|
||||
// Verify database
|
||||
var cfg model.SystemConfig
|
||||
dbConn.Where("key = ?", model.ConfigKeySiteName).First(&cfg)
|
||||
if cfg.Value != "Super Site Name" || cfg.Description != "Updated Description" {
|
||||
if cfg.Value != "Super Site Name" || cfg.Description != "Updated Description" || cfg.Visibility != model.ConfigVisibilityHidden {
|
||||
t.Errorf("database values not updated: %+v", cfg)
|
||||
}
|
||||
|
||||
@@ -244,6 +250,9 @@ func TestUpdateSystemConfig(t *testing.T) {
|
||||
if redisConfig.Value != "Super Site Name" {
|
||||
t.Errorf("redis cache value not updated, got '%s'", redisConfig.Value)
|
||||
}
|
||||
if redisConfig.Visibility != model.ConfigVisibilityHidden {
|
||||
t.Errorf("redis cache visibility = %d, want %d", redisConfig.Visibility, model.ConfigVisibilityHidden)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("update non-existent config", func(t *testing.T) {
|
||||
|
||||
@@ -26,26 +26,9 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PublicConfigResponse 公共配置响应
|
||||
type PublicConfigResponse struct {
|
||||
UploadAllowedExtensions string `json:"upload_allowed_extensions"` // 允许上传的图片扩展名
|
||||
SiteName string `json:"site_name"` // 站点名称
|
||||
PasswordLoginEnabled bool `json:"password_login_enabled"` // 是否允许密码登录
|
||||
RegistrationEnabled bool `json:"registration_enabled"` // 是否允许注册
|
||||
PasswordRegisterEnabled bool `json:"password_register_enabled"` // 是否允许密码注册
|
||||
OIDCLoginEnabled bool `json:"oidc_login_enabled"` // 是否允许 OIDC 登录
|
||||
MaxAPIKeysPerUser int `json:"max_api_keys_per_user"` // 每个用户最大 API Key 数量
|
||||
CapLoginEnabled bool `json:"cap_login_enabled"` // 是否启用人机验证
|
||||
CapAutoSolve bool `json:"cap_auto_solve"` // 打开页面后是否自动开始计算
|
||||
EmailLoginVerificationEnabled bool `json:"email_login_verification_enabled"` // 是否启用邮箱登录验证
|
||||
EmailRegisterVerificationEnabled bool `json:"email_register_verification_enabled"` // 是否启用邮箱注册验证
|
||||
MenuDisplayConfig map[string]bool `json:"menu_display_config"` // 目录显示配置
|
||||
SearchEngineIndexingEnabled bool `json:"search_engine_indexing_enabled"` // 是否允许搜索引擎检索
|
||||
}
|
||||
|
||||
// GetPublicConfig 获取公共配置
|
||||
// @Summary 获取公共配置
|
||||
// @Description 返回对前端公开的系统配置信息,如允许上传的文件类型、站点名称、是否开放注册等
|
||||
// @Description 返回系统配置表中 visibility 为 1 的配置键值集合
|
||||
// @Tags config
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -53,96 +36,15 @@ type PublicConfigResponse struct {
|
||||
// @Router /api/v1/config/public [get]
|
||||
func GetPublicConfig(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
var sc model.SystemConfig
|
||||
|
||||
// 1. upload_allowed_extensions
|
||||
var uploadExtensions string
|
||||
if err := sc.GetByKey(ctx, model.ConfigKeyUploadAllowedExtensions); err == nil {
|
||||
uploadExtensions = sc.Value
|
||||
}
|
||||
|
||||
// 2. site_name
|
||||
var siteName string
|
||||
if err := sc.GetByKey(ctx, model.ConfigKeySiteName); err == nil {
|
||||
siteName = sc.Value
|
||||
}
|
||||
|
||||
// 3. registration_enabled
|
||||
var registrationEnabled bool
|
||||
if val, err := model.GetBoolByKey(ctx, model.ConfigKeyRegistrationEnabled); err == nil {
|
||||
registrationEnabled = val
|
||||
}
|
||||
|
||||
// 3.1 password_login_enabled
|
||||
var passwordLoginEnabled bool
|
||||
if val, err := model.GetBoolByKey(ctx, model.ConfigKeyPasswordLoginEnabled); err == nil {
|
||||
passwordLoginEnabled = val
|
||||
}
|
||||
|
||||
// 3.2 password_register_enabled
|
||||
var passwordRegisterEnabled bool
|
||||
if val, err := model.GetBoolByKey(ctx, model.ConfigKeyPasswordRegisterEnabled); err == nil {
|
||||
passwordRegisterEnabled = val
|
||||
}
|
||||
|
||||
// 3.3 oidc_login_enabled
|
||||
var oidcLoginEnabled bool
|
||||
if val, err := model.GetBoolByKey(ctx, model.ConfigKeyOIDCLoginEnabled); err == nil {
|
||||
oidcLoginEnabled = val
|
||||
}
|
||||
|
||||
// 3.4 cap_login_enabled
|
||||
var capLoginEnabled bool
|
||||
if val, err := model.GetBoolByKey(ctx, model.ConfigKeyCapLoginEnabled); err == nil {
|
||||
capLoginEnabled = val
|
||||
}
|
||||
|
||||
// 3.5 cap_auto_solve
|
||||
capAutoSolve := true // 默认自动开始
|
||||
if val, err := model.GetBoolByKey(ctx, model.ConfigKeyCapAutoSolve); err == nil {
|
||||
capAutoSolve = val
|
||||
}
|
||||
|
||||
// 4. max_api_keys_per_user
|
||||
var maxAPIKeys int
|
||||
if val, err := model.GetIntByKey(ctx, model.ConfigKeyMaxAPIKeysPerUser); err == nil {
|
||||
maxAPIKeys = val
|
||||
}
|
||||
|
||||
var emailLoginVerificationEnabled bool
|
||||
if val, err := model.GetBoolByKey(ctx, model.ConfigKeyEmailLoginVerificationEnabled); err == nil {
|
||||
emailLoginVerificationEnabled = val
|
||||
}
|
||||
|
||||
var emailRegisterVerificationEnabled bool
|
||||
if val, err := model.GetBoolByKey(ctx, model.ConfigKeyEmailRegisterVerificationEnabled); err == nil {
|
||||
emailRegisterVerificationEnabled = val
|
||||
}
|
||||
|
||||
var searchEngineIndexingEnabled bool
|
||||
if val, err := model.GetBoolByKey(ctx, model.ConfigKeySearchEngineIndexingEnabled); err == nil {
|
||||
searchEngineIndexingEnabled = val
|
||||
}
|
||||
|
||||
menuDisplayConfig, err := model.GetMenuDisplayConfig(ctx)
|
||||
configs, err := model.ListVisibleSystemConfigs(ctx)
|
||||
if err != nil {
|
||||
menuDisplayConfig = make(map[string]bool)
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
response := PublicConfigResponse{
|
||||
UploadAllowedExtensions: uploadExtensions,
|
||||
SiteName: siteName,
|
||||
PasswordLoginEnabled: passwordLoginEnabled,
|
||||
RegistrationEnabled: registrationEnabled,
|
||||
PasswordRegisterEnabled: passwordRegisterEnabled,
|
||||
OIDCLoginEnabled: oidcLoginEnabled,
|
||||
MaxAPIKeysPerUser: maxAPIKeys,
|
||||
CapLoginEnabled: capLoginEnabled,
|
||||
CapAutoSolve: capAutoSolve,
|
||||
EmailLoginVerificationEnabled: emailLoginVerificationEnabled,
|
||||
EmailRegisterVerificationEnabled: emailRegisterVerificationEnabled,
|
||||
MenuDisplayConfig: menuDisplayConfig,
|
||||
SearchEngineIndexingEnabled: searchEngineIndexingEnabled,
|
||||
response := make(map[string]string, len(configs))
|
||||
for _, config := range configs {
|
||||
response[config.Key] = config.Value
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(response))
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2026 linux.do
|
||||
Modified by Arctel.net, 2026
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestGetPublicConfigUsesVisibility(t *testing.T) {
|
||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := dbConn.Create(&model.SystemConfig{
|
||||
Key: "custom_public_key",
|
||||
Value: "custom_public_value",
|
||||
Type: "system",
|
||||
Visibility: model.ConfigVisibilityVisible,
|
||||
Description: "custom public config",
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("Create(custom_public_key) error = %v", err)
|
||||
}
|
||||
if err := dbConn.Model(&model.SystemConfig{}).
|
||||
Where("key = ?", model.ConfigKeySiteName).
|
||||
Update("visibility", model.ConfigVisibilityHidden).Error; err != nil {
|
||||
t.Fatalf("Update(%s.visibility) error = %v", model.ConfigKeySiteName, err)
|
||||
}
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.GET("/api/v1/config/public", GetPublicConfig)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/config/public", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("GetPublicConfig() status = %d, want %d; body = %s", w.Code, http.StatusOK, w.Body.String())
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("json.Unmarshal(GetPublicConfig()) error = %v", err)
|
||||
}
|
||||
dataBytes, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal(GetPublicConfig().data) error = %v", err)
|
||||
}
|
||||
var configs map[string]string
|
||||
if err := json.Unmarshal(dataBytes, &configs); err != nil {
|
||||
t.Fatalf("json.Unmarshal(GetPublicConfig().data) error = %v", err)
|
||||
}
|
||||
|
||||
if got := configs["custom_public_key"]; got != "custom_public_value" {
|
||||
t.Errorf("GetPublicConfig()[custom_public_key] = %q, want %q", got, "custom_public_value")
|
||||
}
|
||||
if _, ok := configs[model.ConfigKeySiteName]; ok {
|
||||
t.Errorf("GetPublicConfig()[%s] is present, want hidden", model.ConfigKeySiteName)
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func Migrate() {
|
||||
}
|
||||
|
||||
// ensureConfigKeyExists ensures a system config key exists in the database
|
||||
func ensureConfigKeyExists(key, value, configType, description string) {
|
||||
func ensureConfigKeyExists(key, value, configType, description string, visibility int) {
|
||||
tx := db.DB(context.Background())
|
||||
var cfg model.SystemConfig
|
||||
if err := tx.Where("key = ?", key).First(&cfg).Error; err != nil {
|
||||
@@ -71,6 +71,7 @@ func ensureConfigKeyExists(key, value, configType, description string) {
|
||||
Key: key,
|
||||
Value: value,
|
||||
Type: configType,
|
||||
Visibility: visibility,
|
||||
Description: description,
|
||||
}
|
||||
if err := tx.Create(&newConfig).Error; err != nil {
|
||||
@@ -92,164 +93,49 @@ func initSystemConfigs() {
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
ensureConfigKeyExists(model.ConfigKeyCapLoginEnabled, "false", "system", "是否启用登录人机验证(true/false)")
|
||||
ensureConfigKeyExists(model.ConfigKeyCapAutoSolve, "true", "system", "打开页面后是否自动开始计算,关闭则需用户手动点击触发")
|
||||
ensureConfigKeyExists(model.ConfigKeyCapChallengeCount, "1", "system", "客户端需求解的 PoW 难题总数,默认 1,推荐 1~5")
|
||||
ensureConfigKeyExists(model.ConfigKeyCapChallengeSize, "32", "system", "人机验证盐值长度")
|
||||
ensureConfigKeyExists(model.ConfigKeyCapChallengeDifficulty, "4", "system", "人机验证 PoW 难度(目标前缀长度)")
|
||||
ensureConfigKeyExists(model.ConfigKeyCapChallengeTTL, "600", "system", "人机验证难题有效时间(秒)")
|
||||
ensureConfigKeyExists(model.ConfigKeyCapTokenTTL, "1200", "system", "人机验证兑换凭证有效时间(秒)")
|
||||
ensureConfigKeyExists(model.ConfigKeyServerAddress, "", "system", "服务器地址(用于跨域源控制,不设定则允许任意源)")
|
||||
ensureConfigKeyExists(model.ConfigKeySMTPHost, "", "system", "SMTP 服务器地址(例如 smtp.example.com)")
|
||||
ensureConfigKeyExists(model.ConfigKeySMTPPort, "587", "system", "SMTP 端口(例如 587 或 465)")
|
||||
ensureConfigKeyExists(model.ConfigKeySMTPUsername, "", "system", "SMTP 账户(如 sender@example.com)")
|
||||
ensureConfigKeyExists(model.ConfigKeySMTPPassword, "", "system", "SMTP 访问凭证(授权码/密码)")
|
||||
ensureConfigKeyExists(model.ConfigKeyEmailLoginVerificationEnabled, "false", "system", "是否开启邮箱登录验证(true/false)")
|
||||
ensureConfigKeyExists(model.ConfigKeyEmailRegisterVerificationEnabled, "false", "system", "是否开启邮箱注册验证(true/false)")
|
||||
ensureConfigKeyExists(model.ConfigKeyMenuDisplayConfig, "{}", "system", "目录显示配置(JSON 字符串,格式为 {url: enabled})")
|
||||
ensureConfigKeyExists(model.ConfigKeySearchEngineIndexingEnabled, "false", "system", "是否允许搜索引擎爬取/检索该站点(true/false)")
|
||||
ensureConfigKeyExists(model.ConfigKeyCapLoginEnabled, "false", "system", "是否启用登录人机验证(true/false)", model.ConfigVisibilityVisible)
|
||||
ensureConfigKeyExists(model.ConfigKeyCapAutoSolve, "true", "system", "打开页面后是否自动开始计算,关闭则需用户手动点击触发", model.ConfigVisibilityVisible)
|
||||
ensureConfigKeyExists(model.ConfigKeyCapChallengeCount, "1", "system", "客户端需求解的 PoW 难题总数,默认 1,推荐 1~5", model.ConfigVisibilityHidden)
|
||||
ensureConfigKeyExists(model.ConfigKeyCapChallengeSize, "32", "system", "人机验证盐值长度", model.ConfigVisibilityHidden)
|
||||
ensureConfigKeyExists(model.ConfigKeyCapChallengeDifficulty, "4", "system", "人机验证 PoW 难度(目标前缀长度)", model.ConfigVisibilityHidden)
|
||||
ensureConfigKeyExists(model.ConfigKeyCapChallengeTTL, "600", "system", "人机验证难题有效时间(秒)", model.ConfigVisibilityHidden)
|
||||
ensureConfigKeyExists(model.ConfigKeyCapTokenTTL, "1200", "system", "人机验证兑换凭证有效时间(秒)", model.ConfigVisibilityHidden)
|
||||
ensureConfigKeyExists(model.ConfigKeyServerAddress, "", "system", "服务器地址(用于跨域源控制,不设定则允许任意源)", model.ConfigVisibilityHidden)
|
||||
ensureConfigKeyExists(model.ConfigKeySMTPHost, "", "system", "SMTP 服务器地址(例如 smtp.example.com)", model.ConfigVisibilityHidden)
|
||||
ensureConfigKeyExists(model.ConfigKeySMTPPort, "587", "system", "SMTP 端口(例如 587 或 465)", model.ConfigVisibilityHidden)
|
||||
ensureConfigKeyExists(model.ConfigKeySMTPUsername, "", "system", "SMTP 账户(如 sender@example.com)", model.ConfigVisibilityHidden)
|
||||
ensureConfigKeyExists(model.ConfigKeySMTPPassword, "", "system", "SMTP 访问凭证(授权码/密码)", model.ConfigVisibilityHidden)
|
||||
ensureConfigKeyExists(model.ConfigKeyEmailLoginVerificationEnabled, "false", "system", "是否开启邮箱登录验证(true/false)", model.ConfigVisibilityVisible)
|
||||
ensureConfigKeyExists(model.ConfigKeyEmailRegisterVerificationEnabled, "false", "system", "是否开启邮箱注册验证(true/false)", model.ConfigVisibilityVisible)
|
||||
ensureConfigKeyExists(model.ConfigKeyMenuDisplayConfig, "{}", "system", "目录显示配置(JSON 字符串,格式为 {url: enabled})", model.ConfigVisibilityVisible)
|
||||
ensureConfigKeyExists(model.ConfigKeySearchEngineIndexingEnabled, "false", "system", "是否允许搜索引擎爬取/检索该站点(true/false)", model.ConfigVisibilityVisible)
|
||||
return
|
||||
}
|
||||
|
||||
defaultConfigs := []model.SystemConfig{
|
||||
{
|
||||
Key: model.ConfigKeyCapLoginEnabled,
|
||||
Value: "false",
|
||||
Type: "system",
|
||||
Description: "是否启用登录人机验证(true/false)",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyCapAutoSolve,
|
||||
Value: "true",
|
||||
Type: "system",
|
||||
Description: "打开页面后是否自动开始计算,关闭则需用户手动点击触发",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyCapChallengeCount,
|
||||
Value: "1",
|
||||
Type: "system",
|
||||
Description: "客户端需求解的 PoW 难题总数,默认 1,推荐 1~5",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyCapChallengeSize,
|
||||
Value: "32",
|
||||
Type: "system",
|
||||
Description: "人机验证盐值长度",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyCapChallengeDifficulty,
|
||||
Value: "4",
|
||||
Type: "system",
|
||||
Description: "人机验证 PoW 难度(目标前缀长度)",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyCapChallengeTTL,
|
||||
Value: "600",
|
||||
Type: "system",
|
||||
Description: "人机验证难题有效时间(秒)",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyCapTokenTTL,
|
||||
Value: "1200",
|
||||
Type: "system",
|
||||
Description: "人机验证兑换凭证有效时间(秒)",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyServerAddress,
|
||||
Value: "",
|
||||
Type: "system",
|
||||
Description: "服务器地址(用于跨域源控制,不设定则允许任意源)",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeySMTPHost,
|
||||
Value: "",
|
||||
Type: "system",
|
||||
Description: "SMTP 服务器地址(例如 smtp.example.com)",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeySMTPPort,
|
||||
Value: "587",
|
||||
Type: "system",
|
||||
Description: "SMTP 端口(例如 587 或 465)",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeySMTPUsername,
|
||||
Value: "",
|
||||
Type: "system",
|
||||
Description: "SMTP 账户(如 sender@example.com)",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeySMTPPassword,
|
||||
Value: "",
|
||||
Type: "system",
|
||||
Description: "SMTP 访问凭证(授权码/密码)",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyUploadAllowedExtensions,
|
||||
Value: "jpg,png,webp",
|
||||
Type: "system",
|
||||
Description: "允许上传的图片扩展名(逗号分隔)",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeySiteName,
|
||||
Value: "Wavelet",
|
||||
Type: "system",
|
||||
Description: "系统平台的展示名称",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyPasswordLoginEnabled,
|
||||
Value: "true",
|
||||
Type: "system",
|
||||
Description: "是否允许使用账号密码登录",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyRegistrationEnabled,
|
||||
Value: "true",
|
||||
Type: "system",
|
||||
Description: "控制普通用户是否可以自主注册(true/false)",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyPasswordRegisterEnabled,
|
||||
Value: "true",
|
||||
Type: "system",
|
||||
Description: "是否允许通过密码创建本地账号",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyOIDCLoginEnabled,
|
||||
Value: "true",
|
||||
Type: "system",
|
||||
Description: "是否允许使用第三方 OIDC 认证源登录",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyMaxAPIKeysPerUser,
|
||||
Value: "5",
|
||||
Type: "business",
|
||||
Description: "限制每个普通用户可以创建的 API Key 最大数量",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyEmailLoginVerificationEnabled,
|
||||
Value: "false",
|
||||
Type: "system",
|
||||
Description: "是否开启邮箱登录验证(true/false)",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyEmailRegisterVerificationEnabled,
|
||||
Value: "false",
|
||||
Type: "system",
|
||||
Description: "是否开启邮箱注册验证(true/false)",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeyMenuDisplayConfig,
|
||||
Value: "{}",
|
||||
Type: "system",
|
||||
Description: "目录显示配置(JSON 字符串,格式为 {url: enabled})",
|
||||
},
|
||||
{
|
||||
Key: model.ConfigKeySearchEngineIndexingEnabled,
|
||||
Value: "false",
|
||||
Type: "system",
|
||||
Description: "是否允许搜索引擎爬取/检索该站点(true/false)",
|
||||
},
|
||||
{Key: model.ConfigKeyCapLoginEnabled, Value: "false", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "是否启用登录人机验证(true/false)"},
|
||||
{Key: model.ConfigKeyCapAutoSolve, Value: "true", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "打开页面后是否自动开始计算,关闭则需用户手动点击触发"},
|
||||
{Key: model.ConfigKeyCapChallengeCount, Value: "1", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "客户端需求解的 PoW 难题总数,默认 1,推荐 1~5"},
|
||||
{Key: model.ConfigKeyCapChallengeSize, Value: "32", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "人机验证盐值长度"},
|
||||
{Key: model.ConfigKeyCapChallengeDifficulty, Value: "4", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "人机验证 PoW 难度(目标前缀长度)"},
|
||||
{Key: model.ConfigKeyCapChallengeTTL, Value: "600", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "人机验证难题有效时间(秒)"},
|
||||
{Key: model.ConfigKeyCapTokenTTL, Value: "1200", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "人机验证兑换凭证有效时间(秒)"},
|
||||
{Key: model.ConfigKeyServerAddress, Value: "", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "服务器地址(用于跨域源控制,不设定则允许任意源)"},
|
||||
{Key: model.ConfigKeySMTPHost, Value: "", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "SMTP 服务器地址(例如 smtp.example.com)"},
|
||||
{Key: model.ConfigKeySMTPPort, Value: "587", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "SMTP 端口(例如 587 或 465)"},
|
||||
{Key: model.ConfigKeySMTPUsername, Value: "", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "SMTP 账户(如 sender@example.com)"},
|
||||
{Key: model.ConfigKeySMTPPassword, Value: "", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "SMTP 访问凭证(授权码/密码)"},
|
||||
{Key: model.ConfigKeyUploadAllowedExtensions, Value: "jpg,png,webp", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "允许上传的图片扩展名(逗号分隔)"},
|
||||
{Key: model.ConfigKeySiteName, Value: "Wavelet", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "系统平台的展示名称"},
|
||||
{Key: model.ConfigKeyPasswordLoginEnabled, Value: "true", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "是否允许使用账号密码登录"},
|
||||
{Key: model.ConfigKeyRegistrationEnabled, Value: "true", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "控制普通用户是否可以自主注册(true/false)"},
|
||||
{Key: model.ConfigKeyPasswordRegisterEnabled, Value: "true", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "是否允许通过密码创建本地账号"},
|
||||
{Key: model.ConfigKeyOIDCLoginEnabled, Value: "true", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "是否允许使用第三方 OIDC 认证源登录"},
|
||||
{Key: model.ConfigKeyMaxAPIKeysPerUser, Value: "5", Type: "business", Visibility: model.ConfigVisibilityVisible, Description: "限制每个普通用户可以创建的 API Key 最大数量"},
|
||||
{Key: model.ConfigKeyEmailLoginVerificationEnabled, Value: "false", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "是否开启邮箱登录验证(true/false)"},
|
||||
{Key: model.ConfigKeyEmailRegisterVerificationEnabled, Value: "false", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "是否开启邮箱注册验证(true/false)"},
|
||||
{Key: model.ConfigKeyMenuDisplayConfig, Value: "{}", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "目录显示配置(JSON 字符串,格式为 {url: enabled})"},
|
||||
{Key: model.ConfigKeySearchEngineIndexingEnabled, Value: "false", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "是否允许搜索引擎爬取/检索该站点(true/false)"},
|
||||
}
|
||||
|
||||
if err := tx.Create(&defaultConfigs).Error; err != nil {
|
||||
|
||||
@@ -63,11 +63,19 @@ const (
|
||||
SystemConfigRedisHashKey = "system:system_configs"
|
||||
)
|
||||
|
||||
const (
|
||||
// ConfigVisibilityHidden 表示配置不通过公共配置接口暴露
|
||||
ConfigVisibilityHidden = 0
|
||||
// ConfigVisibilityVisible 表示配置通过公共配置接口暴露
|
||||
ConfigVisibilityVisible = 1
|
||||
)
|
||||
|
||||
// SystemConfig 系统配置实体
|
||||
type SystemConfig struct {
|
||||
Key string `json:"key" gorm:"primaryKey;size:64;not null"`
|
||||
Value string `json:"value" gorm:"size:255;not null"`
|
||||
Type string `json:"type" gorm:"size:32;not null;default:'system'"`
|
||||
Visibility int `json:"visibility" gorm:"not null;default:0"`
|
||||
Description string `json:"description" gorm:"size:255"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
@@ -102,6 +110,21 @@ func (sc *SystemConfig) GetByKey(ctx context.Context, key string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListVisibleSystemConfigs 查询所有可通过公共配置接口暴露的配置
|
||||
func ListVisibleSystemConfigs(ctx context.Context) ([]SystemConfig, error) {
|
||||
database := db.DB(ctx)
|
||||
if database == nil {
|
||||
return nil, errors.New(errDatabaseNotInitialized)
|
||||
}
|
||||
|
||||
var configs []SystemConfig
|
||||
if err := database.Where("visibility = ?", ConfigVisibilityVisible).Find(&configs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// GetIntByKey 通过 key 查询配置并转换为 int 类型
|
||||
func GetIntByKey(ctx context.Context, key string) (int, error) {
|
||||
var sc SystemConfig
|
||||
|
||||
@@ -231,9 +231,37 @@ func seedDefaultConfigs(t *testing.T, tx *gorm.DB) {
|
||||
t.Fatalf("failed to seed default system configs: %v", err)
|
||||
}
|
||||
|
||||
publicKeys := map[string]struct{}{
|
||||
model.ConfigKeyUploadAllowedExtensions: {},
|
||||
model.ConfigKeySiteName: {},
|
||||
model.ConfigKeyPasswordLoginEnabled: {},
|
||||
model.ConfigKeyRegistrationEnabled: {},
|
||||
model.ConfigKeyPasswordRegisterEnabled: {},
|
||||
model.ConfigKeyOIDCLoginEnabled: {},
|
||||
model.ConfigKeyMaxAPIKeysPerUser: {},
|
||||
model.ConfigKeyCapLoginEnabled: {},
|
||||
model.ConfigKeyCapAutoSolve: {},
|
||||
model.ConfigKeyEmailLoginVerificationEnabled: {},
|
||||
model.ConfigKeyEmailRegisterVerificationEnabled: {},
|
||||
model.ConfigKeyMenuDisplayConfig: {},
|
||||
model.ConfigKeySearchEngineIndexingEnabled: {},
|
||||
}
|
||||
keys := make([]string, 0, len(publicKeys))
|
||||
for key := range publicKeys {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
if err := tx.Model(&model.SystemConfig{}).
|
||||
Where("key IN ?", keys).
|
||||
Update("visibility", model.ConfigVisibilityVisible).Error; err != nil {
|
||||
t.Fatalf("failed to seed public system config visibility: %v", err)
|
||||
}
|
||||
|
||||
// Also seed these in miniredis context if required, but they are stored in postgres first.
|
||||
// We'll write configs to miniredis in actual handlers.
|
||||
for _, config := range defaultConfigs {
|
||||
if _, ok := publicKeys[config.Key]; ok {
|
||||
config.Visibility = model.ConfigVisibilityVisible
|
||||
}
|
||||
_ = db.HSetJSON(context.Background(), model.SystemConfigRedisHashKey, config.Key, &config)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user