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:
bigcode
2026-09-30 11:37:38 +08:00
parent d4c1334143
commit 421cfb2f04
47 changed files with 2316 additions and 1431 deletions
+76 -73
View File
@@ -12,7 +12,7 @@ export function setUnauthorizedHandler(fn: () => void): void {
/** # 函数
* 通用请求封装
* 参数: method/url/body(JSON)/query
* 参数: method/url/body(JSON)
* 返回值: 解析后的 JSON;非 2xx 抛 ApiError
*/
export async function api<T = unknown>(
@@ -68,7 +68,7 @@ export function qs(params: Record<string, string | number | undefined>): string
return s ? `?${s}` : "";
}
/* ---- 具体接口封装 ---- */
/* ---- 会话 ---- */
/** 登录 */
export const login = (username: string, password: string) =>
@@ -83,16 +83,19 @@ export const me = () => api<{ user: { id: string; username: string } }>("/api/au
/** 健康检查(含版本号) */
export const health = () => api<{ ok: boolean; name: string; version: string }>("/api/health");
/* ---- 概览 ---- */
/** Dashboard 统计 */
export const stats = () =>
api<{
nodes_online: number;
nodes_total: number;
accounts_online: number;
accounts_total: number;
domains: number;
domains_active: number;
mailboxes: number;
emails: number;
emails_expired: number;
webhooks: number;
global_webhook: number;
deliveries_pending: number;
oauth_connections: number;
recent_emails: { id: string; subject: string; from_address: string; received_at: string; size: number }[];
@@ -101,9 +104,9 @@ export const stats = () =>
/** 手动触发 Cron */
export const runCron = () => api<{ ok: boolean; stats: Record<string, number | string[]> }>("/api/admin/cron/run", "POST");
/* Nodes */
/* ---- 账号(原节点) ---- */
export interface NodeRow {
export interface AccountRow {
id: string;
name: string;
cloudflare_account_id: string | null;
@@ -114,62 +117,79 @@ export interface NodeRow {
created_at: string;
}
export const listNodes = () => api<{ items: NodeRow[]; total: number }>("/api/nodes");
export const listAccounts = () => api<{ items: AccountRow[]; total: number }>("/api/accounts");
export const createNode = (name: string, ingress_url: string) =>
api<{ ok: boolean; node_id: string; enrollment_token: string; ingress_secret: string; worker_name: string }>(
"/api/nodes",
"POST",
{ name, ingress_url },
);
export interface NodeDetail {
node: NodeRow & { enrollment_expires_at: string | null };
export interface AccountDetail {
account: AccountRow & { enrollment_expires_at: string | null };
steps: Record<string, { status: string; error?: string }>;
oauth: { id: string; account_id: string | null; created_at: string } | null;
domains: { id: string; name: string; status: string; worker_id: string | null }[];
}
export const getNode = (id: string) => api<NodeDetail>(`/api/nodes/${id}`);
export const getAccount = (id: string) => api<AccountDetail>(`/api/accounts/${id}`);
export const deleteNode = (id: string) => api(`/api/nodes/${id}`, "DELETE");
export const updateAccount = (id: string, body: { name?: string; ingress_url?: string }) =>
api(`/api/accounts/${id}`, "PATCH", body);
export const rotateNode = (id: string) =>
api<{ ok: boolean; enrollment_token: string }>(`/api/nodes/${id}/rotate`, "POST");
export const deleteAccount = (id: string) => api(`/api/accounts/${id}`, "DELETE");
export const getNodeIngressSecret = (id: string) =>
api<{ ingress_secret: string }>(`/api/nodes/${id}/ingress-secret`);
export const rotateAccount = (id: string) =>
api<{ ok: boolean; enrollment_token: string }>(`/api/accounts/${id}/rotate`, "POST");
/* OAuth */
export const getAccountIngressSecret = (id: string) =>
api<{ ingress_secret: string }>(`/api/accounts/${id}/ingress-secret`);
/* ---- OAuth ---- */
export const oauthMode = () => api<{ mode: "cloudflare" | "sim" }>("/api/oauth/mode");
export const oauthStart = (nodeId: string) =>
api<{ url: string; state: string }>(`/api/oauth/cloudflare/start?node_id=${encodeURIComponent(nodeId)}`);
/** 开始授权:nodeId 缺省 = 自动创建账号 */
export const oauthStart = (nodeId?: string) =>
api<{ url: string; state: string }>(
`/api/oauth/cloudflare/start${nodeId ? `?node_id=${encodeURIComponent(nodeId)}` : ""}`,
);
export const oauthSimApprove = (state: string) =>
api<{ ok: boolean; error?: string; node_id: string }>("/api/oauth/cloudflare/sim/approve", "POST", { state });
/* Domains */
/* ---- 域名 ---- */
export interface DomainRow {
id: string;
name: string;
status: string;
setup_error: string | null;
worker_id: string | null;
zone_id: string | null;
node_id: string | null;
node_name: string | null;
worker_id: string | null;
webhook_url: string | null;
webhook_headers: string | null;
webhook_has_key: number;
mailbox_count: number;
created_at: string;
}
export const listDomains = () => api<{ items: DomainRow[]; total: number }>("/api/domains");
export const createDomain = (name: string) => api<{ ok: boolean; id: string }>("/api/domains", "POST", { name });
/** 域名页账号筛选项 */
export const listDomainAccounts = () => api<{ items: { id: string; name: string }[] }>("/api/domains/filters/accounts");
/** 为域名配置 Email Workers(Worker + Email Routing + catch-all) */
export const setupDomain = (id: string) =>
api<{ ok: boolean; error?: string }>(`/api/domains/${id}/setup`, "POST");
/** 保存域名级 Webhook(url 空串清除;key undefined 保留原值) */
export const putDomainWebhook = (
id: string,
body: { url: string; headers?: Record<string, string>; key?: string },
) => api(`/api/domains/${id}/webhook`, "PUT", body);
export const deleteDomainWebhook = (id: string) => api(`/api/domains/${id}/webhook`, "DELETE");
export const deleteDomain = (id: string) => api(`/api/domains/${id}`, "DELETE");
/* Mailboxes */
/* ---- 邮箱 ---- */
export interface MailboxRow {
id: string;
@@ -179,6 +199,9 @@ export interface MailboxRow {
retention_seconds: number | null;
domain_id: string;
domain_name: string;
webhook_url: string | null;
webhook_headers: string | null;
webhook_has_key: number;
email_count: number;
created_at: string;
}
@@ -192,9 +215,17 @@ export const createMailbox = (body: { domain_id: string; address: string; retent
export const patchMailbox = (id: string, body: { status?: string; retention_seconds?: number | null }) =>
api(`/api/mailboxes/${id}`, "PATCH", body);
/** 保存邮箱级 Webhook(url 空串清除;key undefined 保留原值) */
export const putMailboxWebhook = (
id: string,
body: { url: string; headers?: Record<string, string>; key?: string },
) => api(`/api/mailboxes/${id}/webhook`, "PUT", body);
export const deleteMailboxWebhook = (id: string) => api(`/api/mailboxes/${id}/webhook`, "DELETE");
export const deleteMailbox = (id: string) => api(`/api/mailboxes/${id}`, "DELETE");
/* Emails */
/* ---- 邮件 ---- */
export interface EmailRow {
id: string;
@@ -222,54 +253,26 @@ export const getEmail = (id: string) => api<EmailDetail>(`/api/emails/${id}`);
export const deleteEmail = (id: string) => api(`/api/emails/${id}`, "DELETE");
/* Webhooks */
/* ---- 设置 ---- */
export interface WebhookRow {
id: string;
name: string;
endpoint: string;
enabled: number;
delivery_count: number;
success_count: number;
created_at: string;
export interface SettingsInfo {
username: string;
default_ingress_url: string;
webhook: { url: string; headers: Record<string, string>; has_key: boolean; key_last4: string };
}
export const listWebhooks = () => api<{ items: WebhookRow[]; total: number }>("/api/webhooks");
export const getSettings = () => api<SettingsInfo>("/api/settings");
export const createWebhook = (name: string, endpoint: string) =>
api<{ ok: boolean; id: string; secret: string }>("/api/webhooks", "POST", { name, endpoint });
/** 保存设置;webhook.key:undefined 保留原值、空串清除、非空设置新 key */
export const putSettings = (body: {
default_ingress_url?: string;
webhook?: { url: string; headers?: Record<string, string>; key?: string };
}) => api("/api/settings", "PUT", body);
export const patchWebhook = (id: string, body: { name?: string; endpoint?: string; enabled?: boolean }) =>
api(`/api/webhooks/${id}`, "PATCH", body);
export const changePassword = (old_password: string, new_password: string) =>
api("/api/settings/password", "PUT", { old_password, new_password });
export const rotateWebhookSecret = (id: string) =>
api<{ ok: boolean; secret: string }>(`/api/webhooks/${id}/rotate`, "POST");
export const deleteWebhook = (id: string) => api(`/api/webhooks/${id}`, "DELETE");
export const testWebhook = (id: string) =>
api<{ ok: boolean; status?: number; error?: string }>(`/api/webhooks/${id}/test`, "POST");
export interface DeliveryRow {
id: string;
webhook_id: string;
email_id: string;
status: string;
attempt_count: number;
next_retry_at: string | null;
last_error: string | null;
delivered_at: string | null;
created_at: string;
subject: string | null;
}
export const listDeliveries = (webhookId: string) =>
api<{ items: DeliveryRow[] }>(`/api/webhooks/${webhookId}/deliveries`);
export const redeliver = (deliveryId: string) =>
api(`/api/webhooks/deliveries/${deliveryId}/redeliver`, "POST");
/* API/MCP Keys */
/* ---- API/MCP Keys(设置模态窗口内使用) ---- */
export interface KeyRow {
id: string;