feat: 新增管理后台前端项目及安全加固
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

refactor(saas): 重构认证中间件与限流策略
- 登录限流调整为5次/分钟/IP
- 注册限流调整为3次/小时/IP
- GET请求不计入限流

fix(saas): 修复调度器时间戳处理
- 使用NOW()替代文本时间戳
- 兼容TEXT和TIMESTAMPTZ列类型

feat(saas): 实现环境变量插值
- 支持${ENV_VAR}语法解析
- 数据库密码支持环境变量注入

chore: 新增前端管理界面
- 基于React+Ant Design Pro
- 包含路由守卫/错误边界
- 对接58个API端点

docs: 更新安全加固文档
- 新增密钥管理规范
- 记录P0安全项审计结果
- 补充TLS终止说明

test: 完善配置解析单元测试
- 新增环境变量插值测试用例
This commit is contained in:
iven
2026-03-31 00:11:33 +08:00
parent 6821df5f44
commit eb956d0dce
129 changed files with 11913 additions and 863 deletions

View File

@@ -0,0 +1,165 @@
// ============================================================
// API 密钥管理
// ============================================================
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Button, message, Tag, Modal, Form, Input, InputNumber, Select, Popconfirm, Space, Typography } from 'antd'
import { PlusOutlined, CopyOutlined } from '@ant-design/icons'
import type { ProColumns } from '@ant-design/pro-components'
import { ProTable } from '@ant-design/pro-components'
import { apiKeyService } from '@/services/api-keys'
import type { TokenInfo } from '@/types'
const { Text } = Typography
export default function ApiKeys() {
const queryClient = useQueryClient()
const [form] = Form.useForm()
const [modalOpen, setModalOpen] = useState(false)
const [newToken, setNewToken] = useState<string | null>(null)
const { data, isLoading } = useQuery({
queryKey: ['api-keys'],
queryFn: ({ signal }) => apiKeyService.list(signal),
})
const createMutation = useMutation({
mutationFn: (data: { name: string; expires_days?: number; permissions: string[] }) =>
apiKeyService.create(data),
onSuccess: (result: TokenInfo) => {
message.success('创建成功')
queryClient.invalidateQueries({ queryKey: ['api-keys'] })
if (result.token) {
setNewToken(result.token)
}
setModalOpen(false)
form.resetFields()
},
onError: (err: Error) => message.error(err.message || '创建失败'),
})
const revokeMutation = useMutation({
mutationFn: (id: string) => apiKeyService.revoke(id),
onSuccess: () => {
message.success('已撤销')
queryClient.invalidateQueries({ queryKey: ['api-keys'] })
},
onError: (err: Error) => message.error(err.message || '撤销失败'),
})
const columns: ProColumns<TokenInfo>[] = [
{ title: '名称', dataIndex: 'name', width: 160 },
{ title: '前缀', dataIndex: 'token_prefix', width: 120, render: (_, r) => <Text code>{r.token_prefix}...</Text> },
{
title: '权限',
dataIndex: 'permissions',
width: 200,
render: (_, r) => r.permissions?.map((p) => <Tag key={p}>{p}</Tag>),
},
{
title: '过期时间',
dataIndex: 'expires_at',
width: 180,
render: (_, r) => r.expires_at ? new Date(r.expires_at).toLocaleString('zh-CN') : '永不过期',
},
{
title: '最后使用',
dataIndex: 'last_used_at',
width: 180,
render: (_, r) => r.last_used_at ? new Date(r.last_used_at).toLocaleString('zh-CN') : '-',
},
{
title: '创建时间',
dataIndex: 'created_at',
width: 180,
render: (_, r) => new Date(r.created_at).toLocaleString('zh-CN'),
},
{
title: '操作',
width: 100,
render: (_, record) => (
<Popconfirm title="确定撤销此密钥?撤销后无法恢复。" onConfirm={() => revokeMutation.mutate(record.id)}>
<Button size="small" danger></Button>
</Popconfirm>
),
},
]
const handleCreate = async () => {
const values = await form.validateFields()
createMutation.mutate(values)
}
const copyToken = () => {
if (newToken) {
navigator.clipboard.writeText(newToken)
message.success('已复制到剪贴板')
}
}
return (
<div>
<ProTable<TokenInfo>
columns={columns}
dataSource={data?.items ?? []}
loading={isLoading}
rowKey="id"
search={false}
toolBarRender={() => [
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => { form.resetFields(); setModalOpen(true) }}>
</Button>,
]}
pagination={{
total: data?.total ?? 0,
pageSize: data?.page_size ?? 20,
current: data?.page ?? 1,
showSizeChanger: false,
}}
/>
<Modal
title="创建 API 密钥"
open={modalOpen}
onOk={handleCreate}
onCancel={() => { setModalOpen(false); form.resetFields() }}
confirmLoading={createMutation.isPending}
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input placeholder="给密钥起个名字" />
</Form.Item>
<Form.Item name="expires_days" label="有效期 (天)">
<InputNumber min={1} placeholder="留空则永不过期" style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="permissions" label="权限" rules={[{ required: true }]}>
<Select mode="multiple" placeholder="选择权限" options={[
{ value: 'relay:use', label: '中转使用' },
{ value: 'model:read', label: '模型读取' },
{ value: 'config:read', label: '配置读取' },
]} />
</Form.Item>
</Form>
</Modal>
<Modal
title="密钥创建成功"
open={!!newToken}
onOk={() => setNewToken(null)}
onCancel={() => setNewToken(null)}
>
<p></p>
<Input.TextArea
value={newToken || ''}
rows={3}
readOnly
addonAfter={<CopyOutlined onClick={copyToken} style={{ cursor: 'pointer' }} />}
/>
<Button type="primary" icon={<CopyOutlined />} onClick={copyToken} style={{ marginTop: 8 }}>
</Button>
</Modal>
</div>
)
}