feat: WmailHub v1.0.0 —— Cloudflare Email Workers 邮件接收与转发平台(本地可用版本)
- Console:React 19 + antd 6.6.5 前端(登录 admin/admin)+ Hono 管理 API (Nodes/Domains/Mailboxes/Emails/Webhooks/Keys/OAuth/Admin) - Node:Bun 转发节点(HMAC 签名、有限重试、心跳、结构化日志、优雅退出) - Email Worker:Cloudflare email handler + 本地联调入口 - shared:HMAC 协议(docs/04)、wmh_ Key(32 位)、AES-GCM 加密、SSRF 防护 - 邮件管线:postal-mime 解析、附件、catch-all、限额、malformed 降级、TTL 1 天 - Webhook:签名投递、指数退避重试、SSRF 校验、投递日志 - API v1 + MCP(JSON-RPC,六工具,凭据 kind 隔离) - Cloudflare OAuth:真实 + 本地 sim 模式,自动安装管线(幂等,无需手工命令) - 测试:bun test 41 例(协议安全/管线/TTL/Webhook/SSRF/MCP/OAuth/鉴权)+ Playwright UI 冒烟 - LOGO/favicon 由 codex CLI 生成
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/** 域名页 */
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Button, Card, Form, Input, Modal, Space, Table, Tag, App } from "antd";
|
||||
import { PlusOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||
import { createDomain, deleteDomain, listDomains, type DomainRow } from "../api.ts";
|
||||
|
||||
/** # 函数
|
||||
* 域名页组件
|
||||
*/
|
||||
export function DomainsPage() {
|
||||
const { message, modal } = App.useApp();
|
||||
const [items, setItems] = useState<DomainRow[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
setItems((await listDomains()).items);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-toolbar">
|
||||
<span className="muted">OAuth 安装流程会自动创建域名;此处也可手工登记。</span>
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void load()} />
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setOpen(true)}>
|
||||
添加域名
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Card styles={{ body: { padding: 0 } }}>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
dataSource={items}
|
||||
pagination={false}
|
||||
columns={[
|
||||
{ title: "域名", dataIndex: "name" },
|
||||
{
|
||||
title: "状态",
|
||||
dataIndex: "status",
|
||||
width: 100,
|
||||
render: (v: string) => <Tag color={v === "active" ? "success" : "default"}>{v}</Tag>,
|
||||
},
|
||||
{ title: "节点", dataIndex: "node_name", width: 160, render: (v: string | null) => v ?? "-" },
|
||||
{ title: "Worker", dataIndex: "worker_id", width: 220, render: (v: string | null) => v ?? "-" },
|
||||
{ title: "邮箱数", dataIndex: "mailbox_count", width: 90 },
|
||||
{
|
||||
title: "操作",
|
||||
width: 90,
|
||||
render: (_: unknown, row: DomainRow) => (
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
onClick={() =>
|
||||
modal.confirm({
|
||||
title: `删除域名 ${row.name}?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteDomain(row.id);
|
||||
message.success("已删除");
|
||||
void load();
|
||||
} catch (err) {
|
||||
message.error(err instanceof Error ? err.message : "删除失败");
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="添加域名"
|
||||
open={open}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={async () => {
|
||||
const values = await form.validateFields();
|
||||
try {
|
||||
await createDomain(values.name);
|
||||
message.success("已添加");
|
||||
setOpen(false);
|
||||
form.resetFields();
|
||||
void load();
|
||||
} catch (err) {
|
||||
message.error(err instanceof Error ? err.message : "添加失败");
|
||||
}
|
||||
}}
|
||||
okText="添加"
|
||||
cancelText="取消"
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="name" label="域名" rules={[{ required: true, message: "请输入域名" }]}>
|
||||
<Input placeholder="example.com" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user