Files
zclaw_openfang/admin-temp-dir/src/pages/Accounts.tsx
iven eb956d0dce
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
feat: 新增管理后台前端项目及安全加固
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: 完善配置解析单元测试
- 新增环境变量插值测试用例
2026-03-31 00:11:33 +08:00

171 lines
5.4 KiB
TypeScript

// ============================================================
// 账号管理
// ============================================================
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Button, message, Tag, Modal, Form, Input, Select, Popconfirm, Space } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import type { ProColumns } from '@ant-design/pro-components'
import { ProTable } from '@ant-design/pro-components'
import { accountService } from '@/services/accounts'
import type { AccountPublic } from '@/types'
const roleLabels: Record<string, string> = {
super_admin: '超级管理员',
admin: '管理员',
user: '用户',
}
const roleColors: Record<string, string> = {
super_admin: 'red',
admin: 'blue',
user: 'default',
}
const statusLabels: Record<string, string> = {
active: '正常',
disabled: '已禁用',
suspended: '已封禁',
}
const statusColors: Record<string, string> = {
active: 'green',
disabled: 'default',
suspended: 'red',
}
export default function Accounts() {
const queryClient = useQueryClient()
const [form] = Form.useForm()
const [modalOpen, setModalOpen] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null)
const { data, isLoading } = useQuery({
queryKey: ['accounts'],
queryFn: ({ signal }) => accountService.list(signal),
})
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<AccountPublic> }) =>
accountService.update(id, data),
onSuccess: () => {
message.success('更新成功')
queryClient.invalidateQueries({ queryKey: ['accounts'] })
setModalOpen(false)
},
onError: (err: Error) => message.error(err.message || '更新失败'),
})
const statusMutation = useMutation({
mutationFn: ({ id, status }: { id: string; status: AccountPublic['status'] }) =>
accountService.updateStatus(id, { status }),
onSuccess: () => {
message.success('状态更新成功')
queryClient.invalidateQueries({ queryKey: ['accounts'] })
},
onError: (err: Error) => message.error(err.message || '状态更新失败'),
})
const columns: ProColumns<AccountPublic>[] = [
{ title: '用户名', dataIndex: 'username', width: 120 },
{ title: '显示名', dataIndex: 'display_name', width: 120 },
{ title: '邮箱', dataIndex: 'email', width: 180 },
{
title: '角色',
dataIndex: 'role',
width: 120,
render: (_, record) => <Tag color={roleColors[record.role]}>{roleLabels[record.role] || record.role}</Tag>,
},
{
title: '状态',
dataIndex: 'status',
width: 100,
render: (_, record) => <Tag color={statusColors[record.status]}>{statusLabels[record.status] || record.status}</Tag>,
},
{
title: '2FA',
dataIndex: 'totp_enabled',
width: 80,
render: (_, record) => record.totp_enabled ? <Tag color="green"></Tag> : <Tag></Tag>,
},
{
title: '最后登录',
dataIndex: 'last_login_at',
width: 180,
render: (_, record) => record.last_login_at ? new Date(record.last_login_at).toLocaleString('zh-CN') : '-',
},
{
title: '操作',
width: 200,
render: (_, record) => (
<Space>
<Button size="small" onClick={() => { setEditingId(record.id); form.setFieldsValue(record); setModalOpen(true) }}>
</Button>
{record.status === 'active' ? (
<Popconfirm title="确定禁用此账号?" onConfirm={() => statusMutation.mutate({ id: record.id, status: 'disabled' })}>
<Button size="small" danger></Button>
</Popconfirm>
) : (
<Popconfirm title="确定启用此账号?" onConfirm={() => statusMutation.mutate({ id: record.id, status: 'active' })}>
<Button size="small" type="primary"></Button>
</Popconfirm>
)}
</Space>
),
},
]
const handleSave = async () => {
const values = await form.validateFields()
if (editingId) {
updateMutation.mutate({ id: editingId, data: values })
}
}
return (
<div>
<ProTable<AccountPublic>
columns={columns}
dataSource={data?.items ?? []}
loading={isLoading}
rowKey="id"
search={false}
toolBarRender={() => []}
pagination={{
total: data?.total ?? 0,
pageSize: data?.page_size ?? 20,
current: data?.page ?? 1,
showSizeChanger: false,
}}
/>
<Modal
title="编辑账号"
open={modalOpen}
onOk={handleSave}
onCancel={() => { setModalOpen(false); setEditingId(null); form.resetFields() }}
confirmLoading={updateMutation.isPending}
>
<Form form={form} layout="vertical">
<Form.Item name="display_name" label="显示名">
<Input />
</Form.Item>
<Form.Item name="email" label="邮箱">
<Input type="email" />
</Form.Item>
<Form.Item name="role" label="角色">
<Select options={[
{ value: 'super_admin', label: '超级管理员' },
{ value: 'admin', label: '管理员' },
{ value: 'user', label: '用户' },
]} />
</Form.Item>
</Form>
</Modal>
</div>
)
}
import { useState } from 'react'