mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 23:16:37 +08:00
重构获取公共参数
This commit is contained in:
+18
-1
@@ -2191,7 +2191,7 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"/api/v1/config/public": {
|
"/api/v1/config/public": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "返回对前端公开的系统配置信息,如允许上传的文件类型、站点名称、是否开放注册等",
|
"description": "返回系统配置表中 visibility 为 1 的配置键值集合",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -4008,6 +4008,9 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"value": {
|
"value": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -4465,6 +4468,13 @@ const docTemplate = `{
|
|||||||
"value": {
|
"value": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 255
|
"maxLength": 255
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"type": "integer",
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -4525,6 +4535,13 @@ const docTemplate = `{
|
|||||||
"value": {
|
"value": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 255
|
"maxLength": 255
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"type": "integer",
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+18
-1
@@ -2184,7 +2184,7 @@
|
|||||||
},
|
},
|
||||||
"/api/v1/config/public": {
|
"/api/v1/config/public": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "返回对前端公开的系统配置信息,如允许上传的文件类型、站点名称、是否开放注册等",
|
"description": "返回系统配置表中 visibility 为 1 的配置键值集合",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -4001,6 +4001,9 @@
|
|||||||
},
|
},
|
||||||
"value": {
|
"value": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -4458,6 +4461,13 @@
|
|||||||
"value": {
|
"value": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 255
|
"maxLength": 255
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"type": "integer",
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -4518,6 +4528,13 @@
|
|||||||
"value": {
|
"value": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 255
|
"maxLength": 255
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"type": "integer",
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+13
-1
@@ -250,6 +250,8 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
value:
|
value:
|
||||||
type: string
|
type: string
|
||||||
|
visibility:
|
||||||
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
model.TaskExecution:
|
model.TaskExecution:
|
||||||
properties:
|
properties:
|
||||||
@@ -558,6 +560,11 @@ definitions:
|
|||||||
value:
|
value:
|
||||||
maxLength: 255
|
maxLength: 255
|
||||||
type: string
|
type: string
|
||||||
|
visibility:
|
||||||
|
enum:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
type: integer
|
||||||
required:
|
required:
|
||||||
- key
|
- key
|
||||||
- type
|
- type
|
||||||
@@ -602,6 +609,11 @@ definitions:
|
|||||||
value:
|
value:
|
||||||
maxLength: 255
|
maxLength: 255
|
||||||
type: string
|
type: string
|
||||||
|
visibility:
|
||||||
|
enum:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
type: integer
|
||||||
required:
|
required:
|
||||||
- value
|
- value
|
||||||
type: object
|
type: object
|
||||||
@@ -2206,7 +2218,7 @@ paths:
|
|||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: 返回对前端公开的系统配置信息,如允许上传的文件类型、站点名称、是否开放注册等
|
description: 返回系统配置表中 visibility 为 1 的配置键值集合
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
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() {
|
export function LoginForm() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
@@ -80,7 +85,7 @@ export function LoginForm() {
|
|||||||
const authSourcesQuery = useQuery({
|
const authSourcesQuery = useQuery({
|
||||||
queryKey: ["auth-sources"],
|
queryKey: ["auth-sources"],
|
||||||
queryFn: () => services.auth.getAuthSources(),
|
queryFn: () => services.auth.getAuthSources(),
|
||||||
enabled: publicConfigQuery.data?.oidc_login_enabled ?? true,
|
enabled: configBool(publicConfigQuery.data?.oidc_login_enabled, true),
|
||||||
})
|
})
|
||||||
|
|
||||||
const redirectTarget = useMemo(
|
const redirectTarget = useMemo(
|
||||||
@@ -88,8 +93,8 @@ export function LoginForm() {
|
|||||||
[searchParams],
|
[searchParams],
|
||||||
)
|
)
|
||||||
|
|
||||||
const capEnabled = publicConfigQuery.data?.cap_login_enabled ?? false
|
const capEnabled = configBool(publicConfigQuery.data?.cap_login_enabled, false)
|
||||||
const capAutoSolve = publicConfigQuery.data?.cap_auto_solve ?? true
|
const capAutoSolve = configBool(publicConfigQuery.data?.cap_auto_solve, true)
|
||||||
|
|
||||||
const loginMutation = useMutation({
|
const loginMutation = useMutation({
|
||||||
mutationFn: (req: LoginRequest) => {
|
mutationFn: (req: LoginRequest) => {
|
||||||
@@ -201,7 +206,6 @@ export function LoginForm() {
|
|||||||
|
|
||||||
const handleRegister = () => {
|
const handleRegister = () => {
|
||||||
setErrorMessage("")
|
setErrorMessage("")
|
||||||
const emailRegisterEnabled = publicConfigQuery.data?.email_register_verification_enabled ?? false
|
|
||||||
if (emailRegisterEnabled) {
|
if (emailRegisterEnabled) {
|
||||||
if (!email.trim() || !code.trim()) {
|
if (!email.trim() || !code.trim()) {
|
||||||
toast.error("邮箱和验证码不能为空")
|
toast.error("邮箱和验证码不能为空")
|
||||||
@@ -241,10 +245,11 @@ export function LoginForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const registrationEnabled =
|
const registrationEnabled =
|
||||||
(publicConfigQuery.data?.registration_enabled ?? true) &&
|
configBool(publicConfigQuery.data?.registration_enabled, true) &&
|
||||||
(publicConfigQuery.data?.password_register_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 ?? []
|
const authSources = authSourcesQuery.data ?? []
|
||||||
|
|
||||||
@@ -379,10 +384,10 @@ export function LoginForm() {
|
|||||||
<Input
|
<Input
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder={publicConfigQuery.data?.email_register_verification_enabled ? "电子邮箱" : "电子邮箱(可选)"}
|
placeholder={emailRegisterEnabled ? "电子邮箱" : "电子邮箱(可选)"}
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
/>
|
/>
|
||||||
{publicConfigQuery.data?.email_register_verification_enabled && (
|
{emailRegisterEnabled && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
value={code}
|
value={code}
|
||||||
|
|||||||
@@ -124,6 +124,16 @@ function SystemConfigDetailPanel({
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<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>
|
<label className="text-xs font-medium text-muted-foreground">配置类型</label>
|
||||||
<span className={`text-[11px] px-1.5 py-0.5 rounded font-medium ${
|
<span className={`text-[11px] px-1.5 py-0.5 rounded font-medium ${
|
||||||
@@ -173,6 +183,7 @@ export function SystemConfigs() {
|
|||||||
|
|
||||||
const getInitialEditData = (config: SystemConfig) => ({
|
const getInitialEditData = (config: SystemConfig) => ({
|
||||||
value: config.value,
|
value: config.value,
|
||||||
|
visibility: config.visibility,
|
||||||
description: config.description
|
description: config.description
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -181,6 +192,7 @@ export function SystemConfigs() {
|
|||||||
|
|
||||||
await updateSystemConfig(config.key, {
|
await updateSystemConfig(config.key, {
|
||||||
value: editData.value ?? config.value,
|
value: editData.value ?? config.value,
|
||||||
|
visibility: editData.visibility ?? config.visibility,
|
||||||
description: editData.description ?? config.description
|
description: editData.description ?? config.description
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -208,6 +220,7 @@ export function SystemConfigs() {
|
|||||||
columns={[
|
columns={[
|
||||||
{ header: "配置键", cell: (item) => <span className="font-mono font-medium">{item.key}</span>, width: "200px" },
|
{ 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 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" },
|
{ 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 }) => (
|
renderDetail={({ selected, hovered, editData, onEditDataChange, onSave, saving }) => (
|
||||||
|
|||||||
@@ -234,17 +234,17 @@ export const apiSections: PolicySection[] = [
|
|||||||
<div className="space-y-4 text-sm leading-relaxed">
|
<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>
|
<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>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>
|
<p className="mt-2">返回数据结构样例:</p>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
code={`{
|
code={`{
|
||||||
"error_msg": "",
|
"error_msg": "",
|
||||||
"data": {
|
"data": {
|
||||||
"site_name": "Wavelet",
|
"site_name": "Wavelet",
|
||||||
"registration_enabled": true,
|
"registration_enabled": "true",
|
||||||
"password_login_enabled": true,
|
"password_login_enabled": "true",
|
||||||
"password_register_enabled": true,
|
"password_register_enabled": "true",
|
||||||
"oidc_login_enabled": true
|
"oidc_login_enabled": "true"
|
||||||
}
|
}
|
||||||
}`}
|
}`}
|
||||||
language="json"
|
language="json"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function RobotsMeta() {
|
|||||||
const { config } = usePublicConfig()
|
const { config } = usePublicConfig()
|
||||||
|
|
||||||
// Default to noindex, nofollow if config is loading or if it's explicitly disabled
|
// 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 (
|
return (
|
||||||
<meta
|
<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 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)
|
return data.navMain.filter((item) => displayConfig[item.url] !== false)
|
||||||
}, [config])
|
}, [config])
|
||||||
|
|
||||||
const adminFiltered = React.useMemo(() => {
|
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)
|
return data.admin.filter((item) => displayConfig[item.url] !== false)
|
||||||
}, [config])
|
}, [config])
|
||||||
|
|
||||||
const documentFiltered = React.useMemo(() => {
|
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)
|
return data.document.filter((item) => displayConfig[item.url] !== false)
|
||||||
}, [config])
|
}, [config])
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import {createContext, useCallback, useContext, useRef, useState} from "react"
|
import {createContext, useCallback, useContext, useRef, useState} from "react"
|
||||||
|
import {useQueryClient} from "@tanstack/react-query"
|
||||||
|
|
||||||
import type {SystemConfig, UpdateSystemConfigRequest} from "@/lib/services"
|
import type {SystemConfig, UpdateSystemConfigRequest} from "@/lib/services"
|
||||||
import services from "@/lib/services"
|
import services from "@/lib/services"
|
||||||
@@ -32,6 +33,7 @@ const AdminContext = createContext<AdminContextState | null>(null)
|
|||||||
* @param {React.ReactNode} children - Admin Provider 的子元素
|
* @param {React.ReactNode} children - Admin Provider 的子元素
|
||||||
*/
|
*/
|
||||||
export function AdminProvider({ children }: { children: React.ReactNode }) {
|
export function AdminProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const [systemConfigs, setSystemConfigs] = useState<SystemConfig[]>([])
|
const [systemConfigs, setSystemConfigs] = useState<SystemConfig[]>([])
|
||||||
const [systemConfigsLoading, setSystemConfigsLoading] = useState(false)
|
const [systemConfigsLoading, setSystemConfigsLoading] = useState(false)
|
||||||
const [systemConfigsError, setSystemConfigsError] = useState<Error | null>(null)
|
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) => {
|
const updateSystemConfig = useCallback(async (key: string, data: UpdateSystemConfigRequest) => {
|
||||||
try {
|
try {
|
||||||
await services.admin.updateSystemConfig(key, data)
|
await services.admin.updateSystemConfig(key, data)
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['public-config'] })
|
||||||
await refetchSystemConfigs(lastConfigTypeRef.current)
|
await refetchSystemConfigs(lastConfigTypeRef.current)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleContextError(error, '更新系统配置失败')
|
handleContextError(error, '更新系统配置失败')
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}, [refetchSystemConfigs])
|
}, [queryClient, refetchSystemConfigs])
|
||||||
|
|
||||||
const value: AdminContextState = {
|
const value: AdminContextState = {
|
||||||
systemConfigs,
|
systemConfigs,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface SystemConfig {
|
|||||||
value: string;
|
value: string;
|
||||||
/** 配置类型:'system' | 'business' */
|
/** 配置类型:'system' | 'business' */
|
||||||
type: 'system' | 'business';
|
type: 'system' | 'business';
|
||||||
|
/** 是否对公共配置接口可见:0 不可见,1 可见 */
|
||||||
|
visibility: 0 | 1;
|
||||||
/** 配置描述 */
|
/** 配置描述 */
|
||||||
description: string;
|
description: string;
|
||||||
/** 创建时间 */
|
/** 创建时间 */
|
||||||
@@ -28,6 +30,8 @@ export interface CreateSystemConfigRequest {
|
|||||||
value: string;
|
value: string;
|
||||||
/** 配置类型:'system' | 'business' */
|
/** 配置类型:'system' | 'business' */
|
||||||
type: 'system' | 'business';
|
type: 'system' | 'business';
|
||||||
|
/** 是否对公共配置接口可见:0 不可见,1 可见 */
|
||||||
|
visibility?: 0 | 1;
|
||||||
/** 配置描述(最大255字符,可选) */
|
/** 配置描述(最大255字符,可选) */
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
@@ -38,6 +42,8 @@ export interface CreateSystemConfigRequest {
|
|||||||
export interface UpdateSystemConfigRequest {
|
export interface UpdateSystemConfigRequest {
|
||||||
/** 配置值(最大255字符) */
|
/** 配置值(最大255字符) */
|
||||||
value: string;
|
value: string;
|
||||||
|
/** 是否对公共配置接口可见:0 不可见,1 可见 */
|
||||||
|
visibility?: 0 | 1;
|
||||||
/** 配置描述(最大255字符,可选) */
|
/** 配置描述(最大255字符,可选) */
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,2 @@
|
|||||||
/**
|
/** 公共配置响应:key 为系统配置键,value 为配置表中保存的字符串值 */
|
||||||
* 公共配置响应
|
export type PublicConfigResponse = Record<string, string | undefined>;
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,12 +34,14 @@ type CreateSystemConfigRequest struct {
|
|||||||
Key string `json:"key" binding:"required,max=64"`
|
Key string `json:"key" binding:"required,max=64"`
|
||||||
Value string `json:"value" binding:"required,max=255"`
|
Value string `json:"value" binding:"required,max=255"`
|
||||||
Type string `json:"type" binding:"required,oneof=system business"`
|
Type string `json:"type" binding:"required,oneof=system business"`
|
||||||
|
Visibility int `json:"visibility" binding:"oneof=0 1"`
|
||||||
Description string `json:"description" binding:"max=255"`
|
Description string `json:"description" binding:"max=255"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSystemConfigRequest 更新系统配置请求
|
// UpdateSystemConfigRequest 更新系统配置请求
|
||||||
type UpdateSystemConfigRequest struct {
|
type UpdateSystemConfigRequest struct {
|
||||||
Value string `json:"value" binding:"required,max=255"`
|
Value string `json:"value" binding:"required,max=255"`
|
||||||
|
Visibility *int `json:"visibility" binding:"omitempty,oneof=0 1"`
|
||||||
Description string `json:"description" binding:"max=255"`
|
Description string `json:"description" binding:"max=255"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +80,7 @@ func CreateSystemConfig(c *gin.Context) {
|
|||||||
Key: req.Key,
|
Key: req.Key,
|
||||||
Value: req.Value,
|
Value: req.Value,
|
||||||
Type: req.Type,
|
Type: req.Type,
|
||||||
|
Visibility: req.Visibility,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +209,10 @@ func UpdateSystemConfig(c *gin.Context) {
|
|||||||
updates := map[string]interface{}{
|
updates := map[string]interface{}{
|
||||||
"description": req.Description,
|
"description": req.Description,
|
||||||
}
|
}
|
||||||
|
if req.Visibility != nil {
|
||||||
|
updates["visibility"] = *req.Visibility
|
||||||
|
config.Visibility = *req.Visibility
|
||||||
|
}
|
||||||
if key != model.ConfigKeySMTPPassword || req.Value != "******" {
|
if key != model.ConfigKeySMTPPassword || req.Value != "******" {
|
||||||
updates["value"] = req.Value
|
updates["value"] = req.Value
|
||||||
config.Value = req.Value
|
config.Value = req.Value
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ func TestCreateSystemConfig(t *testing.T) {
|
|||||||
Key: "custom_key",
|
Key: "custom_key",
|
||||||
Value: "custom_value",
|
Value: "custom_value",
|
||||||
Type: "system",
|
Type: "system",
|
||||||
|
Visibility: model.ConfigVisibilityVisible,
|
||||||
Description: "desc",
|
Description: "desc",
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(payload)
|
body, _ := json.Marshal(payload)
|
||||||
@@ -99,7 +100,10 @@ func TestCreateSystemConfig(t *testing.T) {
|
|||||||
t.Fatalf("failed to find system config in Redis: %v", err)
|
t.Fatalf("failed to find system config in Redis: %v", err)
|
||||||
}
|
}
|
||||||
if redisConfig.Value != "custom_value" {
|
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)
|
router := setupTestRouter(adminUser)
|
||||||
|
|
||||||
t.Run("update successfully", func(t *testing.T) {
|
t.Run("update successfully", func(t *testing.T) {
|
||||||
|
hidden := model.ConfigVisibilityHidden
|
||||||
payload := UpdateSystemConfigRequest{
|
payload := UpdateSystemConfigRequest{
|
||||||
Value: "Super Site Name",
|
Value: "Super Site Name",
|
||||||
|
Visibility: &hidden,
|
||||||
Description: "Updated Description",
|
Description: "Updated Description",
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(payload)
|
body, _ := json.Marshal(payload)
|
||||||
@@ -234,7 +240,7 @@ func TestUpdateSystemConfig(t *testing.T) {
|
|||||||
// Verify database
|
// Verify database
|
||||||
var cfg model.SystemConfig
|
var cfg model.SystemConfig
|
||||||
dbConn.Where("key = ?", model.ConfigKeySiteName).First(&cfg)
|
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)
|
t.Errorf("database values not updated: %+v", cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +250,9 @@ func TestUpdateSystemConfig(t *testing.T) {
|
|||||||
if redisConfig.Value != "Super Site Name" {
|
if redisConfig.Value != "Super Site Name" {
|
||||||
t.Errorf("redis cache value not updated, got '%s'", redisConfig.Value)
|
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) {
|
t.Run("update non-existent config", func(t *testing.T) {
|
||||||
|
|||||||
@@ -26,26 +26,9 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"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 获取公共配置
|
// GetPublicConfig 获取公共配置
|
||||||
// @Summary 获取公共配置
|
// @Summary 获取公共配置
|
||||||
// @Description 返回对前端公开的系统配置信息,如允许上传的文件类型、站点名称、是否开放注册等
|
// @Description 返回系统配置表中 visibility 为 1 的配置键值集合
|
||||||
// @Tags config
|
// @Tags config
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@@ -53,96 +36,15 @@ type PublicConfigResponse struct {
|
|||||||
// @Router /api/v1/config/public [get]
|
// @Router /api/v1/config/public [get]
|
||||||
func GetPublicConfig(c *gin.Context) {
|
func GetPublicConfig(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
var sc model.SystemConfig
|
configs, err := model.ListVisibleSystemConfigs(ctx)
|
||||||
|
|
||||||
// 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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
menuDisplayConfig = make(map[string]bool)
|
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := PublicConfigResponse{
|
response := make(map[string]string, len(configs))
|
||||||
UploadAllowedExtensions: uploadExtensions,
|
for _, config := range configs {
|
||||||
SiteName: siteName,
|
response[config.Key] = config.Value
|
||||||
PasswordLoginEnabled: passwordLoginEnabled,
|
|
||||||
RegistrationEnabled: registrationEnabled,
|
|
||||||
PasswordRegisterEnabled: passwordRegisterEnabled,
|
|
||||||
OIDCLoginEnabled: oidcLoginEnabled,
|
|
||||||
MaxAPIKeysPerUser: maxAPIKeys,
|
|
||||||
CapLoginEnabled: capLoginEnabled,
|
|
||||||
CapAutoSolve: capAutoSolve,
|
|
||||||
EmailLoginVerificationEnabled: emailLoginVerificationEnabled,
|
|
||||||
EmailRegisterVerificationEnabled: emailRegisterVerificationEnabled,
|
|
||||||
MenuDisplayConfig: menuDisplayConfig,
|
|
||||||
SearchEngineIndexingEnabled: searchEngineIndexingEnabled,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, util.OK(response))
|
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
|
// 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())
|
tx := db.DB(context.Background())
|
||||||
var cfg model.SystemConfig
|
var cfg model.SystemConfig
|
||||||
if err := tx.Where("key = ?", key).First(&cfg).Error; err != nil {
|
if err := tx.Where("key = ?", key).First(&cfg).Error; err != nil {
|
||||||
@@ -71,6 +71,7 @@ func ensureConfigKeyExists(key, value, configType, description string) {
|
|||||||
Key: key,
|
Key: key,
|
||||||
Value: value,
|
Value: value,
|
||||||
Type: configType,
|
Type: configType,
|
||||||
|
Visibility: visibility,
|
||||||
Description: description,
|
Description: description,
|
||||||
}
|
}
|
||||||
if err := tx.Create(&newConfig).Error; err != nil {
|
if err := tx.Create(&newConfig).Error; err != nil {
|
||||||
@@ -92,164 +93,49 @@ func initSystemConfigs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
ensureConfigKeyExists(model.ConfigKeyCapLoginEnabled, "false", "system", "是否启用登录人机验证(true/false)")
|
ensureConfigKeyExists(model.ConfigKeyCapLoginEnabled, "false", "system", "是否启用登录人机验证(true/false)", model.ConfigVisibilityVisible)
|
||||||
ensureConfigKeyExists(model.ConfigKeyCapAutoSolve, "true", "system", "打开页面后是否自动开始计算,关闭则需用户手动点击触发")
|
ensureConfigKeyExists(model.ConfigKeyCapAutoSolve, "true", "system", "打开页面后是否自动开始计算,关闭则需用户手动点击触发", model.ConfigVisibilityVisible)
|
||||||
ensureConfigKeyExists(model.ConfigKeyCapChallengeCount, "1", "system", "客户端需求解的 PoW 难题总数,默认 1,推荐 1~5")
|
ensureConfigKeyExists(model.ConfigKeyCapChallengeCount, "1", "system", "客户端需求解的 PoW 难题总数,默认 1,推荐 1~5", model.ConfigVisibilityHidden)
|
||||||
ensureConfigKeyExists(model.ConfigKeyCapChallengeSize, "32", "system", "人机验证盐值长度")
|
ensureConfigKeyExists(model.ConfigKeyCapChallengeSize, "32", "system", "人机验证盐值长度", model.ConfigVisibilityHidden)
|
||||||
ensureConfigKeyExists(model.ConfigKeyCapChallengeDifficulty, "4", "system", "人机验证 PoW 难度(目标前缀长度)")
|
ensureConfigKeyExists(model.ConfigKeyCapChallengeDifficulty, "4", "system", "人机验证 PoW 难度(目标前缀长度)", model.ConfigVisibilityHidden)
|
||||||
ensureConfigKeyExists(model.ConfigKeyCapChallengeTTL, "600", "system", "人机验证难题有效时间(秒)")
|
ensureConfigKeyExists(model.ConfigKeyCapChallengeTTL, "600", "system", "人机验证难题有效时间(秒)", model.ConfigVisibilityHidden)
|
||||||
ensureConfigKeyExists(model.ConfigKeyCapTokenTTL, "1200", "system", "人机验证兑换凭证有效时间(秒)")
|
ensureConfigKeyExists(model.ConfigKeyCapTokenTTL, "1200", "system", "人机验证兑换凭证有效时间(秒)", model.ConfigVisibilityHidden)
|
||||||
ensureConfigKeyExists(model.ConfigKeyServerAddress, "", "system", "服务器地址(用于跨域源控制,不设定则允许任意源)")
|
ensureConfigKeyExists(model.ConfigKeyServerAddress, "", "system", "服务器地址(用于跨域源控制,不设定则允许任意源)", model.ConfigVisibilityHidden)
|
||||||
ensureConfigKeyExists(model.ConfigKeySMTPHost, "", "system", "SMTP 服务器地址(例如 smtp.example.com)")
|
ensureConfigKeyExists(model.ConfigKeySMTPHost, "", "system", "SMTP 服务器地址(例如 smtp.example.com)", model.ConfigVisibilityHidden)
|
||||||
ensureConfigKeyExists(model.ConfigKeySMTPPort, "587", "system", "SMTP 端口(例如 587 或 465)")
|
ensureConfigKeyExists(model.ConfigKeySMTPPort, "587", "system", "SMTP 端口(例如 587 或 465)", model.ConfigVisibilityHidden)
|
||||||
ensureConfigKeyExists(model.ConfigKeySMTPUsername, "", "system", "SMTP 账户(如 sender@example.com)")
|
ensureConfigKeyExists(model.ConfigKeySMTPUsername, "", "system", "SMTP 账户(如 sender@example.com)", model.ConfigVisibilityHidden)
|
||||||
ensureConfigKeyExists(model.ConfigKeySMTPPassword, "", "system", "SMTP 访问凭证(授权码/密码)")
|
ensureConfigKeyExists(model.ConfigKeySMTPPassword, "", "system", "SMTP 访问凭证(授权码/密码)", model.ConfigVisibilityHidden)
|
||||||
ensureConfigKeyExists(model.ConfigKeyEmailLoginVerificationEnabled, "false", "system", "是否开启邮箱登录验证(true/false)")
|
ensureConfigKeyExists(model.ConfigKeyEmailLoginVerificationEnabled, "false", "system", "是否开启邮箱登录验证(true/false)", model.ConfigVisibilityVisible)
|
||||||
ensureConfigKeyExists(model.ConfigKeyEmailRegisterVerificationEnabled, "false", "system", "是否开启邮箱注册验证(true/false)")
|
ensureConfigKeyExists(model.ConfigKeyEmailRegisterVerificationEnabled, "false", "system", "是否开启邮箱注册验证(true/false)", model.ConfigVisibilityVisible)
|
||||||
ensureConfigKeyExists(model.ConfigKeyMenuDisplayConfig, "{}", "system", "目录显示配置(JSON 字符串,格式为 {url: enabled})")
|
ensureConfigKeyExists(model.ConfigKeyMenuDisplayConfig, "{}", "system", "目录显示配置(JSON 字符串,格式为 {url: enabled})", model.ConfigVisibilityVisible)
|
||||||
ensureConfigKeyExists(model.ConfigKeySearchEngineIndexingEnabled, "false", "system", "是否允许搜索引擎爬取/检索该站点(true/false)")
|
ensureConfigKeyExists(model.ConfigKeySearchEngineIndexingEnabled, "false", "system", "是否允许搜索引擎爬取/检索该站点(true/false)", model.ConfigVisibilityVisible)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfigs := []model.SystemConfig{
|
defaultConfigs := []model.SystemConfig{
|
||||||
{
|
{Key: model.ConfigKeyCapLoginEnabled, Value: "false", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "是否启用登录人机验证(true/false)"},
|
||||||
Key: model.ConfigKeyCapLoginEnabled,
|
{Key: model.ConfigKeyCapAutoSolve, Value: "true", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "打开页面后是否自动开始计算,关闭则需用户手动点击触发"},
|
||||||
Value: "false",
|
{Key: model.ConfigKeyCapChallengeCount, Value: "1", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "客户端需求解的 PoW 难题总数,默认 1,推荐 1~5"},
|
||||||
Type: "system",
|
{Key: model.ConfigKeyCapChallengeSize, Value: "32", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "人机验证盐值长度"},
|
||||||
Description: "是否启用登录人机验证(true/false)",
|
{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.ConfigKeyCapAutoSolve,
|
{Key: model.ConfigKeyServerAddress, Value: "", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "服务器地址(用于跨域源控制,不设定则允许任意源)"},
|
||||||
Value: "true",
|
{Key: model.ConfigKeySMTPHost, Value: "", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "SMTP 服务器地址(例如 smtp.example.com)"},
|
||||||
Type: "system",
|
{Key: model.ConfigKeySMTPPort, Value: "587", Type: "system", Visibility: model.ConfigVisibilityHidden, Description: "SMTP 端口(例如 587 或 465)"},
|
||||||
Description: "打开页面后是否自动开始计算,关闭则需用户手动点击触发",
|
{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.ConfigKeyCapChallengeCount,
|
{Key: model.ConfigKeySiteName, Value: "Wavelet", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "系统平台的展示名称"},
|
||||||
Value: "1",
|
{Key: model.ConfigKeyPasswordLoginEnabled, Value: "true", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "是否允许使用账号密码登录"},
|
||||||
Type: "system",
|
{Key: model.ConfigKeyRegistrationEnabled, Value: "true", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "控制普通用户是否可以自主注册(true/false)"},
|
||||||
Description: "客户端需求解的 PoW 难题总数,默认 1,推荐 1~5",
|
{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.ConfigKeyCapChallengeSize,
|
{Key: model.ConfigKeyEmailLoginVerificationEnabled, Value: "false", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "是否开启邮箱登录验证(true/false)"},
|
||||||
Value: "32",
|
{Key: model.ConfigKeyEmailRegisterVerificationEnabled, Value: "false", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "是否开启邮箱注册验证(true/false)"},
|
||||||
Type: "system",
|
{Key: model.ConfigKeyMenuDisplayConfig, Value: "{}", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "目录显示配置(JSON 字符串,格式为 {url: enabled})"},
|
||||||
Description: "人机验证盐值长度",
|
{Key: model.ConfigKeySearchEngineIndexingEnabled, Value: "false", Type: "system", Visibility: model.ConfigVisibilityVisible, Description: "是否允许搜索引擎爬取/检索该站点(true/false)"},
|
||||||
},
|
|
||||||
{
|
|
||||||
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)",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Create(&defaultConfigs).Error; err != nil {
|
if err := tx.Create(&defaultConfigs).Error; err != nil {
|
||||||
|
|||||||
@@ -63,11 +63,19 @@ const (
|
|||||||
SystemConfigRedisHashKey = "system:system_configs"
|
SystemConfigRedisHashKey = "system:system_configs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ConfigVisibilityHidden 表示配置不通过公共配置接口暴露
|
||||||
|
ConfigVisibilityHidden = 0
|
||||||
|
// ConfigVisibilityVisible 表示配置通过公共配置接口暴露
|
||||||
|
ConfigVisibilityVisible = 1
|
||||||
|
)
|
||||||
|
|
||||||
// SystemConfig 系统配置实体
|
// SystemConfig 系统配置实体
|
||||||
type SystemConfig struct {
|
type SystemConfig struct {
|
||||||
Key string `json:"key" gorm:"primaryKey;size:64;not null"`
|
Key string `json:"key" gorm:"primaryKey;size:64;not null"`
|
||||||
Value string `json:"value" gorm:"size:255;not null"`
|
Value string `json:"value" gorm:"size:255;not null"`
|
||||||
Type string `json:"type" gorm:"size:32;not null;default:'system'"`
|
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"`
|
Description string `json:"description" gorm:"size:255"`
|
||||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||||
@@ -102,6 +110,21 @@ func (sc *SystemConfig) GetByKey(ctx context.Context, key string) error {
|
|||||||
return nil
|
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 类型
|
// GetIntByKey 通过 key 查询配置并转换为 int 类型
|
||||||
func GetIntByKey(ctx context.Context, key string) (int, error) {
|
func GetIntByKey(ctx context.Context, key string) (int, error) {
|
||||||
var sc SystemConfig
|
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)
|
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.
|
// Also seed these in miniredis context if required, but they are stored in postgres first.
|
||||||
// We'll write configs to miniredis in actual handlers.
|
// We'll write configs to miniredis in actual handlers.
|
||||||
for _, config := range defaultConfigs {
|
for _, config := range defaultConfigs {
|
||||||
|
if _, ok := publicKeys[config.Key]; ok {
|
||||||
|
config.Visibility = model.ConfigVisibilityVisible
|
||||||
|
}
|
||||||
_ = db.HSetJSON(context.Background(), model.SystemConfigRedisHashKey, config.Key, &config)
|
_ = db.HSetJSON(context.Background(), model.SystemConfigRedisHashKey, config.Key, &config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user