feat: v2.0.0 邮件接收架构改造 —— 移除 Node,Email Worker 直连 Console(docs/11)
- 两阶段接收:POST /internal/email/authorize(accept/reject/temporary_error)+ POST /internal/email/ingest(raw MIME 直传) - Worker-Console 协议:Authorization: WmailHub-Worker + timestamp/nonce/body-hash/HMAC,nonce 防重放,凭据撤销即时生效 - 数据模型:nodes/node_credentials -> cloudflare_accounts/worker_credentials(迁移 0003),emails/domains 归属 account_id - 接入流程 Connect Cloudflare Account:OAuth 自动同步域名 + 部署账号级 Email Worker + 签发 Worker 凭据;rotate 自动重部署 - 动态邮箱:创建/删除/禁用下一封邮件立即生效;拒绝码 DOMAIN_*/MAILBOX_*;超时/5xx 不伪装成邮箱不存在 - 删除 node/ 目录、旧 /api/internal/nodes 协议、ingress/enrollment 概念;dev.ts 简化为 console + email worker 双进程 - 前端:账号页 Email Worker/凭据管理,设置窗口移除默认 Ingress,仪表盘/文案同步;版本 2.0.0 - 测试 62 项全过(新增 authorize 决策矩阵 + Worker 两阶段临时错误语义);本机虚拟 E2E 12 场景全通 - docs 00-10 同步改写;README 更新
This commit is contained in:
@@ -28,7 +28,7 @@ const MENU_ITEMS = [
|
||||
/** # 成员变量: 路由 -> 页面标题/副标题 **/
|
||||
const PAGE_META: Record<string, { title: string; subtitle: string }> = {
|
||||
"/": { title: "概览", subtitle: "账号、域名与邮件总览" },
|
||||
"/accounts": { title: "账号", subtitle: "通过 Cloudflare OAuth 接入的 Cloudflare 账号" },
|
||||
"/accounts": { title: "账号", subtitle: "通过 Cloudflare OAuth 接入的 Cloudflare 账号(Email Worker 直连)" },
|
||||
"/domains": { title: "域名", subtitle: "OAuth 自动同步的收件域名与 Email Workers" },
|
||||
"/mailboxes": { title: "邮箱", subtitle: "收件地址、保留策略与邮箱级 Webhook" },
|
||||
"/emails": { title: "邮件", subtitle: "已接收邮件(默认保留 1 天)" },
|
||||
@@ -85,7 +85,7 @@ export function App() {
|
||||
<div className="subtitle">{meta.subtitle}</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 14 }}>
|
||||
<Tooltip title="设置(全局 Webhook / Ingress / 密码 / API & MCP Keys)">
|
||||
<Tooltip title="设置(全局 Webhook / 密码 / API & MCP Keys)">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<SettingOutlined />}
|
||||
|
||||
+20
-20
@@ -88,8 +88,8 @@ export const health = () => api<{ ok: boolean; name: string; version: string }>(
|
||||
/** Dashboard 统计 */
|
||||
export const stats = () =>
|
||||
api<{
|
||||
accounts_online: number;
|
||||
accounts_total: number;
|
||||
accounts_active: number;
|
||||
domains: number;
|
||||
domains_active: number;
|
||||
mailboxes: number;
|
||||
@@ -98,59 +98,61 @@ export const stats = () =>
|
||||
global_webhook: number;
|
||||
deliveries_pending: number;
|
||||
oauth_connections: number;
|
||||
worker_credentials: number;
|
||||
recent_emails: { id: string; subject: string; from_address: string; received_at: string; size: number }[];
|
||||
}>("/api/admin/stats");
|
||||
|
||||
/** 手动触发 Cron */
|
||||
export const runCron = () => api<{ ok: boolean; stats: Record<string, number | string[]> }>("/api/admin/cron/run", "POST");
|
||||
|
||||
/* ---- 账号(原节点) ---- */
|
||||
/* ---- 账号(Cloudflare 账户) ---- */
|
||||
|
||||
export interface AccountRow {
|
||||
id: string;
|
||||
name: string;
|
||||
cloudflare_account_id: string | null;
|
||||
worker_id: string | null;
|
||||
status: string;
|
||||
version: string | null;
|
||||
last_seen_at: string | null;
|
||||
ingress_url: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export const listAccounts = () => api<{ items: AccountRow[]; total: number }>("/api/accounts");
|
||||
|
||||
export interface AccountDetail {
|
||||
account: AccountRow & { enrollment_expires_at: string | null };
|
||||
account: AccountRow;
|
||||
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 }[];
|
||||
credential: { id: string; worker_id: string; secret_last4: string; status: string; created_at: string } | null;
|
||||
}
|
||||
|
||||
export const getAccount = (id: string) => api<AccountDetail>(`/api/accounts/${id}`);
|
||||
|
||||
export const updateAccount = (id: string, body: { name?: string; ingress_url?: string }) =>
|
||||
export const updateAccount = (id: string, body: { name?: string }) =>
|
||||
api(`/api/accounts/${id}`, "PATCH", body);
|
||||
|
||||
export const deleteAccount = (id: string) => api(`/api/accounts/${id}`, "DELETE");
|
||||
|
||||
/** 轮换 Worker credential:secret 仅本次返回可见;已配置域名自动重新部署 */
|
||||
export const rotateAccount = (id: string) =>
|
||||
api<{ ok: boolean; enrollment_token: string }>(`/api/accounts/${id}/rotate`, "POST");
|
||||
api<{ ok: boolean; worker_id: string; credential_id: string; secret: string; redeployed: string[] }>(
|
||||
`/api/accounts/${id}/rotate`,
|
||||
"POST",
|
||||
);
|
||||
|
||||
export const getAccountIngressSecret = (id: string) =>
|
||||
api<{ ingress_secret: string }>(`/api/accounts/${id}/ingress-secret`);
|
||||
/** 本地/运维:查看账号当前 Worker credential(secret 用于部署/联调) */
|
||||
export const getAccountWorkerSecret = (id: string) =>
|
||||
api<{ worker_id: string; secret: string }>(`/api/accounts/${id}/worker-secret`);
|
||||
|
||||
/* ---- OAuth ---- */
|
||||
|
||||
export const oauthMode = () => api<{ mode: "cloudflare" | "sim" }>("/api/oauth/mode");
|
||||
|
||||
/** 开始授权:nodeId 缺省 = 自动创建账号 */
|
||||
export const oauthStart = (nodeId?: string) =>
|
||||
api<{ url: string; state: string }>(
|
||||
`/api/oauth/cloudflare/start${nodeId ? `?node_id=${encodeURIComponent(nodeId)}` : ""}`,
|
||||
);
|
||||
/** 开始授权(自动创建账号记录;授权后自动接入) */
|
||||
export const oauthStart = () => api<{ url: string; state: string }>("/api/oauth/cloudflare/start");
|
||||
|
||||
export const oauthSimApprove = (state: string) =>
|
||||
api<{ ok: boolean; error?: string; node_id: string }>("/api/oauth/cloudflare/sim/approve", "POST", { state });
|
||||
api<{ ok: boolean; error?: string; account_id: string }>("/api/oauth/cloudflare/sim/approve", "POST", { state });
|
||||
|
||||
/* ---- 域名 ---- */
|
||||
|
||||
@@ -161,8 +163,8 @@ export interface DomainRow {
|
||||
setup_error: string | null;
|
||||
worker_id: string | null;
|
||||
zone_id: string | null;
|
||||
node_id: string | null;
|
||||
node_name: string | null;
|
||||
account_id: string | null;
|
||||
account_name: string | null;
|
||||
webhook_url: string | null;
|
||||
webhook_headers: string | null;
|
||||
webhook_has_key: number;
|
||||
@@ -257,7 +259,6 @@ export const deleteEmail = (id: string) => api(`/api/emails/${id}`, "DELETE");
|
||||
|
||||
export interface SettingsInfo {
|
||||
username: string;
|
||||
default_ingress_url: string;
|
||||
webhook: { url: string; headers: Record<string, string>; has_key: boolean; key_last4: string };
|
||||
}
|
||||
|
||||
@@ -265,7 +266,6 @@ export const getSettings = () => api<SettingsInfo>("/api/settings");
|
||||
|
||||
/** 保存设置;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);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 设置模态窗口(v1.1.0):
|
||||
* - 基本设置:全局 Webhook(url/自定义头/key)+ 默认 Node Ingress 地址 + 修改密码
|
||||
* 设置模态窗口(v2.0.0):
|
||||
* - 基本设置:全局 Webhook(url/自定义头/key)+ 修改密码
|
||||
* - API Keys / MCP Keys:wmh_ 凭据的生成、撤销、删除(原 /keys 页面收拢于此)
|
||||
*/
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
@@ -71,12 +71,11 @@ interface WebhookFormValues {
|
||||
}
|
||||
|
||||
/** # 函数
|
||||
* 基本设置:全局 Webhook + 默认 Node Ingress + 修改密码
|
||||
* 基本设置:全局 Webhook + 修改密码
|
||||
*/
|
||||
function GeneralTab({ info, onSaved }: { info: SettingsInfo; onSaved: () => void }) {
|
||||
const { message } = App.useApp();
|
||||
const [webhookForm] = Form.useForm<WebhookFormValues>();
|
||||
const [ingressForm] = Form.useForm<{ ingress: string }>();
|
||||
const [pwdForm] = Form.useForm<{
|
||||
old_password: string;
|
||||
new_password: string;
|
||||
@@ -91,7 +90,7 @@ function GeneralTab({ info, onSaved }: { info: SettingsInfo; onSaved: () => void
|
||||
});
|
||||
}, [info, webhookForm]);
|
||||
|
||||
/** 保存全局 Webhook 与默认 Ingress */
|
||||
/** 保存全局 Webhook */
|
||||
const saveWebhook = async () => {
|
||||
const values = await webhookForm.validateFields();
|
||||
let headers: Record<string, string> = {};
|
||||
@@ -106,7 +105,6 @@ function GeneralTab({ info, onSaved }: { info: SettingsInfo; onSaved: () => void
|
||||
setSaving(true);
|
||||
try {
|
||||
await putSettings({
|
||||
default_ingress_url: info.default_ingress_url,
|
||||
webhook: { url: values.url.trim(), headers, key: values.key },
|
||||
});
|
||||
message.success("全局 Webhook 已保存");
|
||||
@@ -119,18 +117,6 @@ function GeneralTab({ info, onSaved }: { info: SettingsInfo; onSaved: () => void
|
||||
}
|
||||
};
|
||||
|
||||
/** 保存默认 Node Ingress 地址 */
|
||||
const saveIngress = async () => {
|
||||
const values = await ingressForm.validateFields();
|
||||
try {
|
||||
await putSettings({ default_ingress_url: values.ingress.trim() });
|
||||
message.success("默认 Node Ingress 地址已保存");
|
||||
onSaved();
|
||||
} catch (err) {
|
||||
message.error(err instanceof Error ? err.message : "保存失败");
|
||||
}
|
||||
};
|
||||
|
||||
/** 修改密码 */
|
||||
const savePassword = async () => {
|
||||
const values = await pwdForm.validateFields();
|
||||
@@ -179,20 +165,6 @@ function GeneralTab({ info, onSaved }: { info: SettingsInfo; onSaved: () => void
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
<Form form={ingressForm} layout="vertical" initialValues={{ ingress: info.default_ingress_url }}>
|
||||
<Typography.Title level={5}>默认 Node Ingress 地址</Typography.Title>
|
||||
<Typography.Text type="secondary" style={{ display: "block", marginBottom: 8 }}>
|
||||
「添加账号」创建的账号默认使用的 Node Ingress 地址:即 Email Worker 收到邮件后转发到的 Node 接收入口
|
||||
(Node 再签名转发给 Console)。生产环境填 Node 的公网 HTTPS 地址。
|
||||
</Typography.Text>
|
||||
<Space.Compact style={{ width: "100%" }}>
|
||||
<Form.Item name="ingress" noStyle rules={[{ required: true, message: "请输入地址" }]}>
|
||||
<Input placeholder="http://127.0.0.1:18788/ingress/email" />
|
||||
</Form.Item>
|
||||
<Button onClick={() => void saveIngress()}>保存</Button>
|
||||
</Space.Compact>
|
||||
</Form>
|
||||
|
||||
<Form form={pwdForm} layout="vertical" onFinish={() => void savePassword()}>
|
||||
<Typography.Title level={5}>修改密码</Typography.Title>
|
||||
<Form.Item name="old_password" label="原密码" rules={[{ required: true, message: "请输入原密码" }]}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** 账号页(原节点):添加账号直达 Cloudflare OAuth,授权后自动同步域名;搜索、详情、凭据管理 */
|
||||
/** 账号页:添加账号直达 Cloudflare OAuth,授权后自动接入并同步域名;搜索、详情、Worker 凭据管理 */
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
@@ -17,7 +17,6 @@ import { PlusOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons"
|
||||
import {
|
||||
deleteAccount,
|
||||
getAccount,
|
||||
getAccountIngressSecret,
|
||||
listAccounts,
|
||||
oauthMode,
|
||||
oauthStart,
|
||||
@@ -31,17 +30,16 @@ import {
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
pending_oauth: "orange",
|
||||
provisioning: "processing",
|
||||
online: "success",
|
||||
offline: "default",
|
||||
active: "success",
|
||||
error: "error",
|
||||
};
|
||||
|
||||
/** # 成员变量: 安装步骤中文标签(与后端 PROVISION_STEPS 对应) **/
|
||||
/** # 成员变量: 接入步骤中文标签(与后端 PROVISION_STEPS 对应) **/
|
||||
const STEP_LABEL: Record<string, string> = {
|
||||
oauth: "Cloudflare OAuth 授权",
|
||||
identity: "Node identity 与 credential",
|
||||
discover: "Account / Zone 发现",
|
||||
domains: "域名与 catch-all 邮箱同步",
|
||||
worker: "Email Worker 部署与 Worker 凭据",
|
||||
};
|
||||
|
||||
/** # 函数
|
||||
@@ -92,12 +90,11 @@ export function AccountsPage() {
|
||||
[items, q],
|
||||
);
|
||||
|
||||
/** 发起 OAuth:real 模式跳 Cloudflare;sim 模式跳本地模拟授权页。
|
||||
* accountId 缺省 = 服务端自动创建账号。 */
|
||||
const startOAuth = async (accountId?: string) => {
|
||||
/** 发起 OAuth:real 模式跳 Cloudflare;sim 模式跳本地模拟授权页 */
|
||||
const startOAuth = async () => {
|
||||
try {
|
||||
const mode = (await oauthMode()).mode;
|
||||
const { url } = await oauthStart(accountId);
|
||||
const { url } = await oauthStart();
|
||||
if (mode === "cloudflare") window.location.href = url;
|
||||
else navigate(url); // /oauth/sim/authorize?state=...
|
||||
} catch (err) {
|
||||
@@ -122,7 +119,8 @@ export function AccountsPage() {
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
/>
|
||||
<span className="muted">
|
||||
点击「添加账号」完成 Cloudflare OAuth,自动同步账号下全部域名;在域名列表逐域名开启 Email Workers。
|
||||
点击「添加账号」完成 Cloudflare OAuth;Console 自动同步域名、部署 Email Worker
|
||||
并签发 Worker 凭据(Email Worker 直连 Console)。
|
||||
</span>
|
||||
</div>
|
||||
<Space>
|
||||
@@ -149,29 +147,23 @@ export function AccountsPage() {
|
||||
render: (v: string) => <Tag color={STATUS_COLOR[v] ?? "default"}>{v}</Tag>,
|
||||
},
|
||||
{ title: "Cloudflare 账户", dataIndex: "cloudflare_account_id", ellipsis: true, render: (v: string | null) => v ?? "-" },
|
||||
{ title: "Ingress", dataIndex: "ingress_url", ellipsis: true, render: (v: string) => <span className="mono">{v}</span> },
|
||||
{ title: "版本", dataIndex: "version", width: 90, render: (v: string | null) => v ?? "-" },
|
||||
{
|
||||
title: "最近心跳",
|
||||
dataIndex: "last_seen_at",
|
||||
title: "Email Worker",
|
||||
dataIndex: "worker_id",
|
||||
ellipsis: true,
|
||||
render: (v: string | null) => (v ? <span className="mono">{v}</span> : <span className="muted">未部署</span>),
|
||||
},
|
||||
{
|
||||
title: "接入时间",
|
||||
dataIndex: "created_at",
|
||||
width: 170,
|
||||
render: (v: string | null) => (v ? new Date(v).toLocaleString() : "-"),
|
||||
render: (v: string) => new Date(v).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 150,
|
||||
width: 100,
|
||||
render: (_: unknown, row: AccountRow) => (
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
disabled={row.status !== "pending_oauth"}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void startOAuth(row.id);
|
||||
}}
|
||||
>
|
||||
授权
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
@@ -179,7 +171,7 @@ export function AccountsPage() {
|
||||
e.stopPropagation();
|
||||
modal.confirm({
|
||||
title: `删除账号 ${row.name}?`,
|
||||
content: "将撤销账号凭据并标记为 revoked;Node 内部协议将立即失效。",
|
||||
content: "将撤销该账号的 Worker 凭据并标记为 revoked;Email Worker 将无法访问 Console。",
|
||||
onOk: async () => {
|
||||
await deleteAccount(row.id);
|
||||
message.success("已删除");
|
||||
@@ -211,7 +203,7 @@ export function AccountsPage() {
|
||||
}
|
||||
|
||||
/** # 函数
|
||||
* 账号详情抽屉:安装步骤时间线 + 域名 + ingress/凭据管理
|
||||
* 账号详情抽屉:接入步骤时间线 + 域名 + Email Worker 凭据管理
|
||||
*/
|
||||
function AccountDetailDrawer({
|
||||
detail,
|
||||
@@ -225,14 +217,10 @@ function AccountDetailDrawer({
|
||||
onReload: () => void;
|
||||
}) {
|
||||
const { message, modal } = App.useApp();
|
||||
const [ingress, setIngress] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (detail) {
|
||||
setIngress(detail.account.ingress_url);
|
||||
setName(detail.account.name);
|
||||
}
|
||||
if (detail) setName(detail.account.name);
|
||||
}, [detail]);
|
||||
|
||||
if (!detail) return null;
|
||||
@@ -242,7 +230,7 @@ function AccountDetailDrawer({
|
||||
<Drawer title={`账号:${detail.account.name}`} size="large" open={open} onClose={onClose}>
|
||||
<Space orientation="vertical" size="large" style={{ width: "100%" }}>
|
||||
<div>
|
||||
<Typography.Title level={5}>安装进度</Typography.Title>
|
||||
<Typography.Title level={5}>接入进度</Typography.Title>
|
||||
<Timeline
|
||||
items={Object.entries(STEP_LABEL).map(([key, label]) => {
|
||||
const st = steps[key]?.status ?? "pending";
|
||||
@@ -278,58 +266,54 @@ function AccountDetailDrawer({
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card size="small" title="Ingress 与凭据">
|
||||
<Card size="small" title="Email Worker 与凭据">
|
||||
<Space orientation="vertical" style={{ width: "100%" }} size="middle">
|
||||
<Space.Compact style={{ width: "100%" }}>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
style={{ width: 180 }}
|
||||
placeholder="账号名称"
|
||||
/>
|
||||
<Input
|
||||
value={ingress}
|
||||
onChange={(e) => setIngress(e.target.value)}
|
||||
placeholder="Node Ingress 地址"
|
||||
/>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await updateAccount(detail.account.id, { name, ingress_url: ingress });
|
||||
message.success("已保存(新邮件将从新 Ingress 转发)");
|
||||
await updateAccount(detail.account.id, { name });
|
||||
message.success("已保存");
|
||||
onReload();
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<Typography.Text type="secondary" className="mono">
|
||||
Worker ID:{detail.account.worker_id ?? "-"}
|
||||
{detail.credential ? ` · 凭据 ****${detail.credential.secret_last4}(有效)` : ""}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ display: "block" }}>
|
||||
Email Worker 以该凭据直连 Console(/internal/email/authorize 与 /ingest);轮换后已配置的域名会自动重新部署。
|
||||
</Typography.Text>
|
||||
<Space wrap>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const res = await rotateAccount(detail.account.id);
|
||||
modal.info({
|
||||
title: "新的 Enrollment Token(仅显示一次)",
|
||||
content: <Typography.Paragraph copyable className="mono">{res.enrollment_token}</Typography.Paragraph>,
|
||||
title: "新的 Worker Secret(仅显示一次)",
|
||||
content: (
|
||||
<div>
|
||||
<Typography.Paragraph copyable className="mono" style={{ marginBottom: 4 }}>
|
||||
{res.secret}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Text type="secondary">
|
||||
已自动重新部署域名:{res.redeployed.length ? res.redeployed.join("、") : "无(尚无已配置域名)"}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
onReload();
|
||||
}}
|
||||
>
|
||||
轮换凭据(生成新 Enrollment Token)
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const res = await getAccountIngressSecret(detail.account.id);
|
||||
modal.info({
|
||||
title: "Ingress Secret(Email Worker -> Node 共享密钥)",
|
||||
content: <Typography.Paragraph copyable className="mono">{res.ingress_secret}</Typography.Paragraph>,
|
||||
});
|
||||
}}
|
||||
>
|
||||
查看 Ingress Secret
|
||||
轮换 Worker 凭据
|
||||
</Button>
|
||||
</Space>
|
||||
<Typography.Text type="secondary" className="mono">
|
||||
enrollment 有效期至 {detail.account.enrollment_expires_at ?? "-"}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Card>
|
||||
</Space>
|
||||
|
||||
@@ -49,7 +49,7 @@ export function DashboardPage() {
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={12} md={8} lg={4}>
|
||||
<Card loading={loading}>
|
||||
<Statistic title="账号在线" value={data?.accounts_online ?? 0} suffix={`/ ${data?.accounts_total ?? 0}`} />
|
||||
<Statistic title="Cloudflare 账号" value={data?.accounts_active ?? 0} suffix={`/ ${data?.accounts_total ?? 0}`} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} md={8} lg={4}>
|
||||
|
||||
@@ -58,7 +58,7 @@ export function DomainsPage() {
|
||||
() =>
|
||||
items.filter(
|
||||
(row) =>
|
||||
(!accountId || row.node_id === accountId) &&
|
||||
(!accountId || row.account_id === accountId) &&
|
||||
row.name.toLowerCase().includes(q.trim().toLowerCase()),
|
||||
),
|
||||
[items, q, accountId],
|
||||
@@ -110,7 +110,7 @@ export function DomainsPage() {
|
||||
onRow={(row) => ({ onClick: () => setEditing(row), style: { cursor: "pointer" } })}
|
||||
columns={[
|
||||
{ title: "域名", dataIndex: "name", render: (v: string) => <span className="mono">{v}</span> },
|
||||
{ title: "账号", dataIndex: "node_name", width: 170, ellipsis: true, render: (v: string | null) => v ?? "-" },
|
||||
{ title: "账号", dataIndex: "account_name", width: 170, ellipsis: true, render: (v: string | null) => v ?? "-" },
|
||||
{ title: "状态", dataIndex: "status", width: 170, render: (_: unknown, row: DomainRow) => <StatusTag row={row} /> },
|
||||
{
|
||||
title: "Email Worker",
|
||||
@@ -174,7 +174,7 @@ export function DomainsPage() {
|
||||
<span>
|
||||
<span className="mono">{editing.name}</span>
|
||||
<Typography.Text type="secondary" style={{ marginLeft: 8, fontWeight: 400 }}>
|
||||
{editing.node_name ?? "-"}
|
||||
{editing.account_name ?? "-"}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -20,7 +20,7 @@ export function OAuthSimPage() {
|
||||
const res = await oauthSimApprove(state);
|
||||
if (res.ok) {
|
||||
message.success("Cloudflare 授权与自动安装完成");
|
||||
navigate(`/accounts?provision=${encodeURIComponent(res.node_id)}`, { replace: true });
|
||||
navigate(`/accounts?provision=${encodeURIComponent(res.account_id)}`, { replace: true });
|
||||
} else {
|
||||
message.error(res.error ?? "安装失败");
|
||||
navigate("/accounts", { replace: true });
|
||||
|
||||
Reference in New Issue
Block a user