mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
重构获取公共参数
This commit is contained in:
+18
-1
@@ -2191,7 +2191,7 @@ const docTemplate = `{
|
||||
},
|
||||
"/api/v1/config/public": {
|
||||
"get": {
|
||||
"description": "返回对前端公开的系统配置信息,如允许上传的文件类型、站点名称、是否开放注册等",
|
||||
"description": "返回系统配置表中 visibility 为 1 的配置键值集合",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -4008,6 +4008,9 @@ const docTemplate = `{
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
"visibility": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4465,6 +4468,13 @@ const docTemplate = `{
|
||||
"value": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"visibility": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
0,
|
||||
1
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4525,6 +4535,13 @@ const docTemplate = `{
|
||||
"value": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"visibility": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
0,
|
||||
1
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+18
-1
@@ -2184,7 +2184,7 @@
|
||||
},
|
||||
"/api/v1/config/public": {
|
||||
"get": {
|
||||
"description": "返回对前端公开的系统配置信息,如允许上传的文件类型、站点名称、是否开放注册等",
|
||||
"description": "返回系统配置表中 visibility 为 1 的配置键值集合",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -4001,6 +4001,9 @@
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
"visibility": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4458,6 +4461,13 @@
|
||||
"value": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"visibility": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
0,
|
||||
1
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4518,6 +4528,13 @@
|
||||
"value": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"visibility": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
0,
|
||||
1
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+13
-1
@@ -250,6 +250,8 @@ definitions:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
visibility:
|
||||
type: integer
|
||||
type: object
|
||||
model.TaskExecution:
|
||||
properties:
|
||||
@@ -558,6 +560,11 @@ definitions:
|
||||
value:
|
||||
maxLength: 255
|
||||
type: string
|
||||
visibility:
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
type: integer
|
||||
required:
|
||||
- key
|
||||
- type
|
||||
@@ -602,6 +609,11 @@ definitions:
|
||||
value:
|
||||
maxLength: 255
|
||||
type: string
|
||||
visibility:
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
type: integer
|
||||
required:
|
||||
- value
|
||||
type: object
|
||||
@@ -2206,7 +2218,7 @@ paths:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 返回对前端公开的系统配置信息,如允许上传的文件类型、站点名称、是否开放注册等
|
||||
description: 返回系统配置表中 visibility 为 1 的配置键值集合
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
||||
@@ -36,6 +36,11 @@ function persistRedirectTarget(searchParams: ReturnType<typeof useSearchParams>)
|
||||
}
|
||||
}
|
||||
|
||||
function configBool(value: string | undefined, fallback: boolean) {
|
||||
if (value === undefined) return fallback
|
||||
return value === "true"
|
||||
}
|
||||
|
||||
export function LoginForm() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
@@ -80,7 +85,7 @@ export function LoginForm() {
|
||||
const authSourcesQuery = useQuery({
|
||||
queryKey: ["auth-sources"],
|
||||
queryFn: () => services.auth.getAuthSources(),
|
||||
enabled: publicConfigQuery.data?.oidc_login_enabled ?? true,
|
||||
enabled: configBool(publicConfigQuery.data?.oidc_login_enabled, true),
|
||||
})
|
||||
|
||||
const redirectTarget = useMemo(
|
||||
@@ -88,8 +93,8 @@ export function LoginForm() {
|
||||
[searchParams],
|
||||
)
|
||||
|
||||
const capEnabled = publicConfigQuery.data?.cap_login_enabled ?? false
|
||||
const capAutoSolve = publicConfigQuery.data?.cap_auto_solve ?? true
|
||||
const capEnabled = configBool(publicConfigQuery.data?.cap_login_enabled, false)
|
||||
const capAutoSolve = configBool(publicConfigQuery.data?.cap_auto_solve, true)
|
||||
|
||||
const loginMutation = useMutation({
|
||||
mutationFn: (req: LoginRequest) => {
|
||||
@@ -201,7 +206,6 @@ export function LoginForm() {
|
||||
|
||||
const handleRegister = () => {
|
||||
setErrorMessage("")
|
||||
const emailRegisterEnabled = publicConfigQuery.data?.email_register_verification_enabled ?? false
|
||||
if (emailRegisterEnabled) {
|
||||
if (!email.trim() || !code.trim()) {
|
||||
toast.error("邮箱和验证码不能为空")
|
||||
@@ -241,10 +245,11 @@ export function LoginForm() {
|
||||
}
|
||||
|
||||
const registrationEnabled =
|
||||
(publicConfigQuery.data?.registration_enabled ?? true) &&
|
||||
(publicConfigQuery.data?.password_register_enabled ?? true)
|
||||
configBool(publicConfigQuery.data?.registration_enabled, true) &&
|
||||
configBool(publicConfigQuery.data?.password_register_enabled, true)
|
||||
|
||||
const passwordLoginEnabled = publicConfigQuery.data?.password_login_enabled ?? true
|
||||
const passwordLoginEnabled = configBool(publicConfigQuery.data?.password_login_enabled, true)
|
||||
const emailRegisterEnabled = configBool(publicConfigQuery.data?.email_register_verification_enabled, false)
|
||||
|
||||
const authSources = authSourcesQuery.data ?? []
|
||||
|
||||
@@ -379,10 +384,10 @@ export function LoginForm() {
|
||||
<Input
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder={publicConfigQuery.data?.email_register_verification_enabled ? "电子邮箱" : "电子邮箱(可选)"}
|
||||
placeholder={emailRegisterEnabled ? "电子邮箱" : "电子邮箱(可选)"}
|
||||
autoComplete="email"
|
||||
/>
|
||||
{publicConfigQuery.data?.email_register_verification_enabled && (
|
||||
{emailRegisterEnabled && (
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={code}
|
||||
|
||||
@@ -124,6 +124,16 @@ function SystemConfigDetailPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0">
|
||||
<label className="text-xs font-medium text-muted-foreground">公共可见</label>
|
||||
<Switch
|
||||
checked={(editData.visibility ?? config?.visibility ?? 0) === 1}
|
||||
onCheckedChange={(checked) => {
|
||||
onEditDataChange('visibility', checked ? 1 : 0)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0">
|
||||
<label className="text-xs font-medium text-muted-foreground">配置类型</label>
|
||||
<span className={`text-[11px] px-1.5 py-0.5 rounded font-medium ${
|
||||
@@ -173,6 +183,7 @@ export function SystemConfigs() {
|
||||
|
||||
const getInitialEditData = (config: SystemConfig) => ({
|
||||
value: config.value,
|
||||
visibility: config.visibility,
|
||||
description: config.description
|
||||
})
|
||||
|
||||
@@ -181,6 +192,7 @@ export function SystemConfigs() {
|
||||
|
||||
await updateSystemConfig(config.key, {
|
||||
value: editData.value ?? config.value,
|
||||
visibility: editData.visibility ?? config.visibility,
|
||||
description: editData.description ?? config.description
|
||||
})
|
||||
}
|
||||
@@ -208,6 +220,7 @@ export function SystemConfigs() {
|
||||
columns={[
|
||||
{ header: "配置键", cell: (item) => <span className="font-mono font-medium">{item.key}</span>, width: "200px" },
|
||||
{ header: "配置值", cell: (item) => <span className="truncate max-w-[120px] inline-block" title={item.value}>{item.value}</span>, width: "120px" },
|
||||
{ header: "公共可见", cell: (item) => <span>{item.visibility === 1 ? "可见" : "不可见"}</span>, width: "80px" },
|
||||
{ header: "描述", cell: (item) => <span className="truncate max-w-[200px] inline-block text-muted-foreground" title={item.description}>{item.description}</span>, width: "200px" },
|
||||
]}
|
||||
renderDetail={({ selected, hovered, editData, onEditDataChange, onSave, saving }) => (
|
||||
|
||||
@@ -234,17 +234,17 @@ export const apiSections: PolicySection[] = [
|
||||
<div className="space-y-4 text-sm leading-relaxed">
|
||||
<h3 id="4-1-public-config" className="text-base md:text-lg font-semibold text-foreground mt-4 mb-2">4.1 公共系统配置</h3>
|
||||
<p><strong>接口:</strong>GET <code className="bg-muted px-1.5 py-0.5 rounded text-xs font-mono">/api/v1/config/public</code></p>
|
||||
<p><strong>说明:</strong>无感获取当前系统的公开业务设置(如注册是否开启、密码登录是否开启)。供前端页面动态渲染使用。</p>
|
||||
<p><strong>说明:</strong>无感获取当前系统配置表中公共可见的键值集合。供前端页面动态渲染使用。</p>
|
||||
<p className="mt-2">返回数据结构样例:</p>
|
||||
<CodeBlock
|
||||
code={`{
|
||||
"error_msg": "",
|
||||
"data": {
|
||||
"site_name": "Wavelet",
|
||||
"registration_enabled": true,
|
||||
"password_login_enabled": true,
|
||||
"password_register_enabled": true,
|
||||
"oidc_login_enabled": true
|
||||
"registration_enabled": "true",
|
||||
"password_login_enabled": "true",
|
||||
"password_register_enabled": "true",
|
||||
"oidc_login_enabled": "true"
|
||||
}
|
||||
}`}
|
||||
language="json"
|
||||
|
||||
@@ -6,7 +6,7 @@ export function RobotsMeta() {
|
||||
const { config } = usePublicConfig()
|
||||
|
||||
// Default to noindex, nofollow if config is loading or if it's explicitly disabled
|
||||
const enabled = config?.search_engine_indexing_enabled === true
|
||||
const enabled = config?.search_engine_indexing_enabled === "true"
|
||||
|
||||
return (
|
||||
<meta
|
||||
|
||||
@@ -84,6 +84,23 @@ const data = {
|
||||
],
|
||||
}
|
||||
|
||||
function parseMenuDisplayConfig(raw: string | undefined): Record<string, boolean> {
|
||||
if (!raw) return {}
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(raw)
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}
|
||||
|
||||
return Object.entries(parsed).reduce<Record<string, boolean>>((result, [key, value]) => {
|
||||
if (typeof value === "boolean") {
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}, {})
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用侧边栏组件
|
||||
* 显示应用侧边栏
|
||||
@@ -145,17 +162,17 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
}
|
||||
|
||||
const navMainFiltered = React.useMemo(() => {
|
||||
const displayConfig = config?.menu_display_config || {}
|
||||
const displayConfig = parseMenuDisplayConfig(config?.menu_display_config)
|
||||
return data.navMain.filter((item) => displayConfig[item.url] !== false)
|
||||
}, [config])
|
||||
|
||||
const adminFiltered = React.useMemo(() => {
|
||||
const displayConfig = config?.menu_display_config || {}
|
||||
const displayConfig = parseMenuDisplayConfig(config?.menu_display_config)
|
||||
return data.admin.filter((item) => displayConfig[item.url] !== false)
|
||||
}, [config])
|
||||
|
||||
const documentFiltered = React.useMemo(() => {
|
||||
const displayConfig = config?.menu_display_config || {}
|
||||
const displayConfig = parseMenuDisplayConfig(config?.menu_display_config)
|
||||
return data.document.filter((item) => displayConfig[item.url] !== false)
|
||||
}, [config])
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
import {createContext, useCallback, useContext, useRef, useState} from "react"
|
||||
import {useQueryClient} from "@tanstack/react-query"
|
||||
|
||||
import type {SystemConfig, UpdateSystemConfigRequest} from "@/lib/services"
|
||||
import services from "@/lib/services"
|
||||
@@ -32,6 +33,7 @@ const AdminContext = createContext<AdminContextState | null>(null)
|
||||
* @param {React.ReactNode} children - Admin Provider 的子元素
|
||||
*/
|
||||
export function AdminProvider({ children }: { children: React.ReactNode }) {
|
||||
const queryClient = useQueryClient()
|
||||
const [systemConfigs, setSystemConfigs] = useState<SystemConfig[]>([])
|
||||
const [systemConfigsLoading, setSystemConfigsLoading] = useState(false)
|
||||
const [systemConfigsError, setSystemConfigsError] = useState<Error | null>(null)
|
||||
@@ -70,12 +72,13 @@ export function AdminProvider({ children }: { children: React.ReactNode }) {
|
||||
const updateSystemConfig = useCallback(async (key: string, data: UpdateSystemConfigRequest) => {
|
||||
try {
|
||||
await services.admin.updateSystemConfig(key, data)
|
||||
await queryClient.invalidateQueries({ queryKey: ['public-config'] })
|
||||
await refetchSystemConfigs(lastConfigTypeRef.current)
|
||||
} catch (error) {
|
||||
handleContextError(error, '更新系统配置失败')
|
||||
throw error
|
||||
}
|
||||
}, [refetchSystemConfigs])
|
||||
}, [queryClient, refetchSystemConfigs])
|
||||
|
||||
const value: AdminContextState = {
|
||||
systemConfigs,
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface SystemConfig {
|
||||
value: string;
|
||||
/** 配置类型:'system' | 'business' */
|
||||
type: 'system' | 'business';
|
||||
/** 是否对公共配置接口可见:0 不可见,1 可见 */
|
||||
visibility: 0 | 1;
|
||||
/** 配置描述 */
|
||||
description: string;
|
||||
/** 创建时间 */
|
||||
@@ -28,6 +30,8 @@ export interface CreateSystemConfigRequest {
|
||||
value: string;
|
||||
/** 配置类型:'system' | 'business' */
|
||||
type: 'system' | 'business';
|
||||
/** 是否对公共配置接口可见:0 不可见,1 可见 */
|
||||
visibility?: 0 | 1;
|
||||
/** 配置描述(最大255字符,可选) */
|
||||
description?: string;
|
||||
}
|
||||
@@ -38,6 +42,8 @@ export interface CreateSystemConfigRequest {
|
||||
export interface UpdateSystemConfigRequest {
|
||||
/** 配置值(最大255字符) */
|
||||
value: string;
|
||||
/** 是否对公共配置接口可见:0 不可见,1 可见 */
|
||||
visibility?: 0 | 1;
|
||||
/** 配置描述(最大255字符,可选) */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,2 @@
|
||||
/**
|
||||
* 公共配置响应
|
||||
*/
|
||||
export interface PublicConfigResponse {
|
||||
/** 允许上传的图片扩展名 */
|
||||
upload_allowed_extensions: string;
|
||||
/** 站点名称 */
|
||||
site_name: string;
|
||||
/** 是否允许密码登录 */
|
||||
password_login_enabled: boolean;
|
||||
/** 是否允许注册 */
|
||||
registration_enabled: boolean;
|
||||
/** 是否允许密码注册 */
|
||||
password_register_enabled: boolean;
|
||||
/** 是否允许 OIDC 登录 */
|
||||
oidc_login_enabled: boolean;
|
||||
/** 每个用户最大 API Key 数量 */
|
||||
max_api_keys_per_user: number;
|
||||
/** 是否启用 Cap 人机验证 */
|
||||
cap_login_enabled: boolean;
|
||||
/** 是否自动解题 */
|
||||
cap_auto_solve: boolean;
|
||||
/** 是否启用邮箱登录验证 */
|
||||
email_login_verification_enabled: boolean;
|
||||
/** 是否启用邮箱注册验证 */
|
||||
email_register_verification_enabled: boolean;
|
||||
/** 目录显示配置 */
|
||||
menu_display_config?: Record<string, boolean>;
|
||||
/** 是否允许搜索引擎检索 */
|
||||
search_engine_indexing_enabled?: boolean;
|
||||
}
|
||||
/** 公共配置响应:key 为系统配置键,value 为配置表中保存的字符串值 */
|
||||
export type PublicConfigResponse = Record<string, string | undefined>;
|
||||
|
||||
@@ -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