v2.5.0:操作日志 + 邮件保留 + 账号弹窗精简 + 复制按钮推广
- 操作日志:operation_logs 表(0004 迁移)记录 账号/域名/邮箱/Webhook 全部写操作, 含操作账号(Console 登录名,系统动作为 system);/logs 日志页(类型筛选+搜索+分页); 上限 10000 条,Cron housekeeping 超限删最旧 - 邮件保留:emails.retained;POST /api/emails/retain(单条/批量); TTL 清理与 Webhook 投递过期判定均跳过已保留;/inboxs 操作列图钉按钮 + 批量保留 - 添加账号弹窗:OAuth 与 API Token 同窗按钮组切换,去掉说明文字与账户 ID 字段 (账户 ID 以 Token 实际所属账户为准);账号列表去掉 Email Worker 列 - 复制按钮推广:CopyText 组件;/domains 域名列、/inboxs 发件人与收件人(原「邮箱」改名)列 - 测试:104 bun tests(+10 日志/保留)、ui-smoke 35 项(+logs 页/新弹窗/复制/保留)
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
/** 账号页:添加账号直达 Cloudflare OAuth,授权后自动接入并同步域名;搜索、详情、Worker 凭据管理 */
|
||||
/** 账号页:添加账号(Cloudflare OAuth / API Token 同窗切换),授权后自动接入并同步域名;搜索、详情、凭据管理 */
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Radio,
|
||||
Space,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Timeline,
|
||||
Typography,
|
||||
@@ -137,7 +136,6 @@ export function AccountsPage() {
|
||||
try {
|
||||
const res = await createAccountByToken({
|
||||
name: values.name,
|
||||
cloudflare_account_id: values.cloudflare_account_id?.trim() || undefined,
|
||||
token: values.token.trim(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -179,10 +177,7 @@ export function AccountsPage() {
|
||||
style={{ width: 280 }}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
/>
|
||||
<span className="muted">
|
||||
「添加账号」支持 Cloudflare OAuth 授权与 API Token 两种方式;接入后自动同步域名、
|
||||
部署 Email Worker 并签发 Worker 凭据(Email Worker 直连 Console)。
|
||||
</span>
|
||||
<span className="muted">接入后自动同步域名、部署 Email Worker 并签发 Worker 凭据(Email Worker 直连 Console)。</span>
|
||||
</div>
|
||||
<Space>
|
||||
<BatchActions
|
||||
@@ -236,12 +231,6 @@ 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: "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",
|
||||
@@ -320,9 +309,9 @@ interface AddAccountModalProps {
|
||||
}
|
||||
|
||||
/** # 函数
|
||||
* 「添加账号」模态窗口:Cloudflare OAuth 与 API Token 两种授权方式,各自带说明。
|
||||
* 两种方式最终都走同一套自动接入流程(发现域名 -> 部署 Email Worker -> 签发 Worker 凭据),
|
||||
* 区别只在于 token 从哪来:OAuth 走用户点授权,API Token 由用户自己粘贴。
|
||||
* 「添加 Cloudflare 账号」模态窗口:Cloudflare OAuth 授权与 API Token 授权在同一个窗口,
|
||||
* 顶部按钮组切换方式,字段随方式变化 —— OAuth 只要一个「前往授权」按钮,
|
||||
* API Token 填账户名称 + Token 即可(账户 ID 由 Token 在 Cloudflare 侧实际所属账户自动确定)。
|
||||
*/
|
||||
function AddAccountModal(props: AddAccountModalProps) {
|
||||
const { open, mode, loading, form, onModeChange, onCancel, onOAuth, onTokenSubmit } = props;
|
||||
@@ -333,96 +322,41 @@ function AddAccountModal(props: AddAccountModalProps) {
|
||||
onCancel={onCancel}
|
||||
maskClosable={!loading}
|
||||
closable={!loading}
|
||||
width={620}
|
||||
width={520}
|
||||
footer={null}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Tabs
|
||||
activeKey={mode}
|
||||
onChange={(k) => onModeChange(k as "oauth" | "token")}
|
||||
items={[
|
||||
{
|
||||
key: "oauth",
|
||||
label: (
|
||||
<span>
|
||||
<SafetyOutlined /> Cloudflare OAuth 授权
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<Space orientation="vertical" size="middle" style={{ width: "100%" }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="推荐:不需要在本机保存任何密钥"
|
||||
description={
|
||||
<span>
|
||||
点击下方按钮跳转到 Cloudflare 官方授权页,用你的 Cloudflare 账号登录并同意授权,
|
||||
完成后自动跳回本控制台。Console 会用拿到的授权去发现该账号下的域名、
|
||||
部署 Email Worker 并签发 Worker 凭据,全程无需在本系统里保存 Cloudflare 密钥。
|
||||
<br />
|
||||
前提:「设置 → Cloudflare 接入」里已填好 Client ID / Secret,且 OAuth App
|
||||
已勾选本应用需要的权限(scope 用点号分隔)。
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<Button type="primary" icon={<SafetyOutlined />} onClick={onOAuth} block>
|
||||
前往 Cloudflare 授权
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "token",
|
||||
label: (
|
||||
<span>
|
||||
<ApiOutlined /> API Token 授权
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<Form form={form} layout="vertical" requiredMark="optional">
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message="适合不方便走 OAuth 跳转的场景"
|
||||
description={
|
||||
<span>
|
||||
直接粘贴一个 Cloudflare API Token。Token 会在提交时先被 Cloudflare 校验一遍
|
||||
(并与填写的账户 ID 比对),确认可用后才建账号并自动接入,填错会当场报错。
|
||||
Token 加密入库、不回显、不写日志。
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="账户名称"
|
||||
rules={[{ required: true, message: "请输入账户名称" }]}
|
||||
>
|
||||
<Input placeholder="我的主账号" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="cloudflare_account_id"
|
||||
label="账户 ID"
|
||||
extra="Cloudflare 账户 ID(账户主页右侧的 Account ID)。可留空,留空则以 Token 实际所属账户为准;填错会被校验拦下。"
|
||||
>
|
||||
<Input placeholder="bf8cd983e6dfd171c1881b00b512ed5a" className="mono" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="token"
|
||||
label="API Token"
|
||||
rules={[{ required: true, message: "请输入 API Token" }]}
|
||||
extra="到 Cloudflare → My Profile → API Tokens 生成。所需权限与 OAuth 方式一致:Zone Read、Workers Scripts Edit、Email Routing Rules Edit、Zone Settings Write。"
|
||||
>
|
||||
<Input.Password placeholder="粘贴 Cloudflare API Token" autoComplete="new-password" />
|
||||
</Form.Item>
|
||||
<Button type="primary" icon={<ApiOutlined />} onClick={onTokenSubmit} loading={loading} block>
|
||||
{loading ? "正在校验并接入…" : "用 API Token 接入"}
|
||||
</Button>
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Radio.Group
|
||||
block
|
||||
value={mode}
|
||||
onChange={(e) => onModeChange(e.target.value as "oauth" | "token")}
|
||||
style={{ marginBottom: 4 }}
|
||||
>
|
||||
<Radio.Button value="oauth" style={{ width: "50%" }}>
|
||||
<SafetyOutlined /> Cloudflare OAuth 授权
|
||||
</Radio.Button>
|
||||
<Radio.Button value="token" style={{ width: "50%" }}>
|
||||
<ApiOutlined /> API Token 授权
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Divider style={{ margin: "16px 0" }} />
|
||||
{mode === "oauth" ? (
|
||||
<Button type="primary" icon={<SafetyOutlined />} onClick={onOAuth} block>
|
||||
前往 Cloudflare 授权
|
||||
</Button>
|
||||
) : (
|
||||
<Form form={form} layout="vertical" requiredMark="optional">
|
||||
<Form.Item name="name" label="账户名称" rules={[{ required: true, message: "请输入账户名称" }]}>
|
||||
<Input placeholder="我的主账号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="token" label="API Token" rules={[{ required: true, message: "请输入 API Token" }]}>
|
||||
<Input.Password placeholder="粘贴 Cloudflare API Token" autoComplete="new-password" />
|
||||
</Form.Item>
|
||||
<Button type="primary" icon={<ApiOutlined />} onClick={onTokenSubmit} loading={loading} block>
|
||||
{loading ? "正在校验并接入…" : "用 API Token 接入"}
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -483,9 +417,7 @@ function AccountDetailDrawer({
|
||||
detail.domains.map((d) => (
|
||||
<div key={d.id}>
|
||||
<Tag color="blue">{d.name}</Tag>
|
||||
<span className="muted mono">
|
||||
{d.worker_id ?? "未配置 Email Workers"}
|
||||
</span>
|
||||
<span className="muted">{d.worker_id ? "已配置 Email Workers" : "未配置 Email Workers"}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user