feat: v1.1.0 账号化重构 + 三级 Webhook + 设置中心 + Vite 工具链
- 账号化:节点更名账号,/nodes -> /accounts;添加账号直达 Cloudflare OAuth,自动创建账号并同步账号下全部域名(catch-all 邮箱随同步创建) - 域名:移除手工添加;点击域名『设置 Email Workers』= 部署账号级 Email Worker + Email Routing + catch-all;支持按账号筛选 - Webhook 三级模型:邮箱 -> 域名 -> 全局,收信取第一个命中者;支持自定义请求头与 key(默认 Authorization: Bearer <key>,兼做 X-WmailHub-Signature 签名);独立 webhooks 实体与 /webhooks、/keys 页面删除,Key 管理收进设置模态窗口 - 设置中心:全局 Webhook / 默认 Node Ingress 地址 / 修改密码 / API & MCP Keys(生成、撤销、删除) - UI:Header 标题副标题紧凑化(修复溢出),LOGO 加版本号 tag(v1.1.0 取自 package.json),各列表页搜索框 - 工具链:前端构建 Bun.build -> Vite(vite 8 + @vitejs/plugin-react) - 后端:/api/settings、POST /api/domains/:id/setup、0002 迁移(投递行固化三级目标配置);MCP 去 list_webhooks(5 工具);迁移执行器加 _migrations 记录 - 测试:44/44(新增 settings 套件、webhook 三级优先级/SSRF/签名);UI 冒烟 11/11
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Webhook 配置表单(域名级/邮箱级共用;全局版本在设置模态窗口内)。
|
||||
* key 语义:留空保留现有;填空格清除;非空设置新 key。
|
||||
*/
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Form, Input, Space, App } from "antd";
|
||||
import { putDomainWebhook, putMailboxWebhook } from "../api.ts";
|
||||
|
||||
/** # 成员变量: 表单当前值(由父组件传入) **/
|
||||
export interface WebhookValue {
|
||||
url: string | null;
|
||||
headers: string | null;
|
||||
hasKey: boolean;
|
||||
}
|
||||
|
||||
/** # 函数
|
||||
* 解析 headers JSON 串为对象(失败返回 {})
|
||||
*/
|
||||
export function parseHeaders(s: string | null): Record<string, string> {
|
||||
if (!s) return {};
|
||||
try {
|
||||
const v = JSON.parse(s);
|
||||
return v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, string>) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** # 函数
|
||||
* Webhook 配置表单组件
|
||||
* 参数: scope - "domain" | "mailbox";targetId - 域名/邮箱 ID;value - 当前配置;onSaved - 保存后回调
|
||||
*/
|
||||
export function WebhookForm({
|
||||
scope,
|
||||
targetId,
|
||||
value,
|
||||
onSaved,
|
||||
}: {
|
||||
scope: "domain" | "mailbox";
|
||||
targetId: string;
|
||||
value: WebhookValue;
|
||||
onSaved: () => void;
|
||||
}) {
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm<{ url: string; headers: string; key?: string }>();
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
url: value.url ?? "",
|
||||
headers: value.url ? JSON.stringify(parseHeaders(value.headers), null, 2) : "",
|
||||
});
|
||||
}, [value, form, targetId]);
|
||||
|
||||
const save = async () => {
|
||||
const values = await form.validateFields();
|
||||
let headers: Record<string, string> = {};
|
||||
if (values.headers.trim()) {
|
||||
try {
|
||||
headers = JSON.parse(values.headers) as Record<string, string>;
|
||||
} catch {
|
||||
message.error("自定义请求头必须是合法 JSON 对象");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const key = values.key ?? undefined;
|
||||
// "填空格清除 key":把单个空格翻译成空串
|
||||
const normalizedKey = key !== undefined && key.trim() === "" && key.length > 0 ? "" : key;
|
||||
setSaving(true);
|
||||
try {
|
||||
const body = { url: values.url.trim(), headers, key: normalizedKey };
|
||||
if (scope === "domain") await putDomainWebhook(targetId, body);
|
||||
else await putMailboxWebhook(targetId, body);
|
||||
message.success("Webhook 已保存");
|
||||
form.setFieldValue("key", undefined);
|
||||
onSaved();
|
||||
} catch (err) {
|
||||
message.error(err instanceof Error ? err.message : "保存失败");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clear = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
if (scope === "domain") await putDomainWebhook(targetId, { url: "" });
|
||||
else await putMailboxWebhook(targetId, { url: "" });
|
||||
message.success("已清除该级 Webhook(将回退到上一级配置)");
|
||||
form.resetFields();
|
||||
onSaved();
|
||||
} catch (err) {
|
||||
message.error(err instanceof Error ? err.message : "清除失败");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="url" label="Webhook URL" extra="留空并保存 = 清除该级配置">
|
||||
<Input placeholder="https://hooks.example.com/wmail" allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="headers"
|
||||
label="自定义请求头(JSON 对象,可选)"
|
||||
extra='示例:{"X-Token":"abc"};配置了 key 时默认携带 Authorization: Bearer <key>'
|
||||
>
|
||||
<Input.TextArea rows={2} placeholder='{"X-Custom":"1"}' />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="key"
|
||||
label="Key"
|
||||
extra={
|
||||
value.hasKey
|
||||
? "已设置 key(****尾号见保存后提示);留空保留,填空格清除"
|
||||
: "可选;默认以 Authorization: Bearer <key> 携带,并用于投递签名"
|
||||
}
|
||||
>
|
||||
<Input.Password placeholder="留空保留现有 key" autoComplete="new-password" />
|
||||
</Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" loading={saving} onClick={() => void save()}>
|
||||
保存
|
||||
</Button>
|
||||
{value.url && (
|
||||
<Button danger onClick={() => void clear()}>
|
||||
清除该级配置
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user