feat(ConfigGroup, SystemConfig): add support for secret fields in configuration forms

This commit is contained in:
shiyu
2025-05-25 12:08:15 +08:00
parent 6866580dce
commit a5c7959c4a
3 changed files with 83 additions and 44 deletions
+1
View File
@@ -195,6 +195,7 @@ export interface ConfigResponse {
key: string; key: string;
value: string; value: string;
description: string; description: string;
isSecret: boolean;
createdAt: Date; createdAt: Date;
updatedAt?: Date; updatedAt?: Date;
} }
+23 -5
View File
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Form, Input, Button, Space, Row, Col, Tooltip } from 'antd'; import { Form, Input, Button, Space, Row, Col, Tooltip } from 'antd';
import { SaveOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { SaveOutlined, QuestionCircleOutlined, LockOutlined } from '@ant-design/icons';
interface ConfigGroupProps { interface ConfigGroupProps {
groupName: string; groupName: string;
@@ -11,6 +11,7 @@ interface ConfigGroupProps {
descriptions: { descriptions: {
[key: string]: string; [key: string]: string;
}; };
secretFields?: string[];
isMobile?: boolean; isMobile?: boolean;
} }
@@ -19,6 +20,7 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
configs, configs,
onSave, onSave,
descriptions, descriptions,
secretFields = [],
isMobile = false isMobile = false
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -45,6 +47,10 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
} }
}; };
const isSecretField = (key: string): boolean => {
return secretFields.includes(key);
};
return ( return (
<Form <Form
form={form} form={form}
@@ -52,7 +58,10 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
initialValues={configs} initialValues={configs}
size={isMobile ? "middle" : "large"} size={isMobile ? "middle" : "large"}
> >
{Object.keys(configs).map(key => ( {Object.keys(configs).map(key => {
const isSecret = isSecretField(key);
return (
<Row key={key} gutter={isMobile ? [8, 8] : [16, 16]} align="middle"> <Row key={key} gutter={isMobile ? [8, 8] : [16, 16]} align="middle">
<Col xs={24} lg={16}> <Col xs={24} lg={16}>
<Form.Item <Form.Item
@@ -60,6 +69,7 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
label={ label={
<Space> <Space>
{key} {key}
{isSecret && <LockOutlined style={{ color: '#faad14' }} />}
{descriptions[key] && ( {descriptions[key] && (
<Tooltip title={descriptions[key]}> <Tooltip title={descriptions[key]}>
<QuestionCircleOutlined /> <QuestionCircleOutlined />
@@ -67,9 +77,16 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
)} )}
</Space> </Space>
} }
extra={isSecret &&
<div style={{ fontSize: '12px', color: '#faad14', marginTop: '4px' }}>
</div>
}
> >
{key.toLowerCase().includes('secret') || key.toLowerCase().includes('key') || key.toLowerCase().includes('password') ? ( {isSecret ? (
<Input.Password placeholder={`请输入${key}`} /> <Input.Password
placeholder={configs[key] === '' ? '请输入新值' : '******(已设置,输入新值以更新)'}
/>
) : ( ) : (
<Input placeholder={`请输入${key}`} /> <Input placeholder={`请输入${key}`} />
)} )}
@@ -94,7 +111,8 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
</Button> </Button>
</Col> </Col>
</Row> </Row>
))} );
})}
<Form.Item> <Form.Item>
<Button <Button
+20
View File
@@ -24,6 +24,7 @@ const SystemConfig: React.FC = () => {
const [restoreLoading, setRestoreLoading] = useState(false); const [restoreLoading, setRestoreLoading] = useState(false);
const [restoreModalVisible, setRestoreModalVisible] = useState(false); const [restoreModalVisible, setRestoreModalVisible] = useState(false);
const [restoreConfig, setRestoreConfig] = useState<Record<string, string> | null>(null); const [restoreConfig, setRestoreConfig] = useState<Record<string, string> | null>(null);
const [secretFields, setSecretFields] = useState<Record<string, string[]>>({}); // 新增状态管理私密字段
// 获取所有配置项 // 获取所有配置项
const fetchConfigs = async () => { const fetchConfigs = async () => {
@@ -32,15 +33,27 @@ const SystemConfig: React.FC = () => {
const response = await getAllConfigs(); const response = await getAllConfigs();
if (response.success && response.data) { if (response.success && response.data) {
const configGroups: ConfigStructure = {}; const configGroups: ConfigStructure = {};
const secretFieldsMap: Record<string, string[]> = {}; // 记录每个组的私密字段
response.data.forEach(config => { response.data.forEach(config => {
const [group, key] = config.key.split(':'); const [group, key] = config.key.split(':');
if (!configGroups[group]) { if (!configGroups[group]) {
configGroups[group] = {}; configGroups[group] = {};
secretFieldsMap[group] = [];
} }
configGroups[group][key] = config.value; configGroups[group][key] = config.value;
// 记录私密字段
if (config.isSecret) {
if (!secretFieldsMap[group]) {
secretFieldsMap[group] = [];
}
secretFieldsMap[group].push(key);
}
}); });
setConfigs(configGroups); setConfigs(configGroups);
setSecretFields(secretFieldsMap);
// 设置初始存储类型 // 设置初始存储类型
if (configGroups.Storage?.DefaultStorage) { if (configGroups.Storage?.DefaultStorage) {
@@ -245,6 +258,7 @@ const SystemConfig: React.FC = () => {
Model: 'AI 模型名称', Model: 'AI 模型名称',
EmbeddingModel: '嵌入向量模型名称' EmbeddingModel: '嵌入向量模型名称'
}} }}
secretFields={secretFields.AI || []}
isMobile={isMobile} isMobile={isMobile}
/> />
</TabPane> </TabPane>
@@ -265,6 +279,7 @@ const SystemConfig: React.FC = () => {
Issuer: 'JWT 签发者', Issuer: 'JWT 签发者',
Audience: 'JWT 接收者', Audience: 'JWT 接收者',
}} }}
secretFields={secretFields.Jwt || []}
isMobile={isMobile} isMobile={isMobile}
/> />
</TabPane> </TabPane>
@@ -282,6 +297,7 @@ const SystemConfig: React.FC = () => {
"GitHubClientSecret": 'GitHub OAuth 应用客户端密钥', "GitHubClientSecret": 'GitHub OAuth 应用客户端密钥',
"GitHubCallbackUrl": 'GitHub OAuth 认证回调地址' "GitHubCallbackUrl": 'GitHub OAuth 认证回调地址'
}} }}
secretFields={secretFields.Authentication || []}
isMobile={isMobile} isMobile={isMobile}
/> />
</TabPane> </TabPane>
@@ -543,6 +559,7 @@ const SystemConfig: React.FC = () => {
"TelegramStorageBotToken": 'Telegram 机器人令牌', "TelegramStorageBotToken": 'Telegram 机器人令牌',
"TelegramStorageChatId": 'Telegram 聊天ID' "TelegramStorageChatId": 'Telegram 聊天ID'
}} }}
secretFields={secretFields.Storage || []}
isMobile={isMobile} isMobile={isMobile}
/> />
)} )}
@@ -569,6 +586,7 @@ const SystemConfig: React.FC = () => {
"S3StorageCdnUrl": 'CDN URL (可选,用于加速文件访问)', "S3StorageCdnUrl": 'CDN URL (可选,用于加速文件访问)',
"S3StorageUsePathStyleUrls": '使用路径形式URLs (true/false,兼容非AWS服务)' "S3StorageUsePathStyleUrls": '使用路径形式URLs (true/false,兼容非AWS服务)'
}} }}
secretFields={secretFields.Storage || []}
isMobile={isMobile} isMobile={isMobile}
/> />
)} )}
@@ -593,6 +611,7 @@ const SystemConfig: React.FC = () => {
"CosStorageRegion": 'COS区域 (例如:ap-shanghai)', "CosStorageRegion": 'COS区域 (例如:ap-shanghai)',
"CosStorageCdnUrl": 'CDN URL (可选,用于加速文件访问)', "CosStorageCdnUrl": 'CDN URL (可选,用于加速文件访问)',
}} }}
secretFields={secretFields.Storage || []}
isMobile={isMobile} isMobile={isMobile}
/> />
)} )}
@@ -615,6 +634,7 @@ const SystemConfig: React.FC = () => {
"WebDAVBasePath": 'WebDAV 基础路径 (例如: files/upload)', "WebDAVBasePath": 'WebDAV 基础路径 (例如: files/upload)',
"WebDAVPublicUrl": 'WebDAV 公共访问 URL (可选,用于文件访问)', "WebDAVPublicUrl": 'WebDAV 公共访问 URL (可选,用于文件访问)',
}} }}
secretFields={secretFields.Storage || []}
isMobile={isMobile} isMobile={isMobile}
/> />
)} )}