feat: v2.3.0 设置内配置 Cloudflare OAuth 客户端 + 全列表批量删除

- 设置新增「Cloudflare 接入」tab:Client ID / Client Secret(Secret AES-GCM 加密入库,
  只回显末 4 位),并展示可复制的授权回调地址;settings 优先、env 变量兜底,
  保存后立即生效无需重新部署;DELETE /api/settings/cf-oauth 清除
- 四个资源列表(账号/域名/邮箱/收件箱)加复选框批量选择与批量删除
- 新增 POST /api/{accounts,domains,mailboxes,emails}/batch-delete:
  账号软删+撤销凭据;域名/邮箱级联删除其下邮件;邮件连 R2 原文与附件一并清理
- 抽出 deleteEmailById / deleteEmailsByMailbox / revokeAccount 供单条与批量共用
- 点击复选框不再触发行点击(避免误开详情抽屉)
- 新增 tests/batch.test.ts(6 例)与 settings OAuth 凭据用例(2 例);ui-smoke 扩到 17 步
This commit is contained in:
bigcode
2026-09-30 15:53:18 +08:00
parent e2c3c353cf
commit 3be6e483b4
19 changed files with 1001 additions and 79 deletions
+3 -3
View File
@@ -109,7 +109,7 @@ export interface ProvisionInput {
* 部署账号级 Email Worker + 签发 Worker credential。幂等,可重复调用;返回是否全部成功。
*/
export async function provisionAccount(env: Env, input: ProvisionInput): Promise<{ ok: boolean; error?: string }> {
const cf: CFApi = getCFApi(env);
const cf: CFApi = await getCFApi(env);
const account = await env.DB.prepare("SELECT id, name, status FROM cloudflare_accounts WHERE id = ?")
.bind(input.accountId)
.first<{ id: string; name: string; status: string }>();
@@ -249,7 +249,7 @@ async function putWorkerScript(
workerId: string,
workerSecret: string,
): Promise<void> {
const cf: CFApi = getCFApi(env);
const cf: CFApi = await getCFApi(env);
const consoleUrl = env.PUBLIC_BASE_URL ?? "";
if (!consoleUrl) throw new Error("console public url unknown (PUBLIC_BASE_URL)");
await cf.putEmailWorker({ accessToken, accountId: cfAccountId, scriptName: workerId, consoleUrl, workerId, workerSecret });
@@ -278,7 +278,7 @@ export async function setupDomainEmail(env: Env, domainId: string): Promise<{ ok
const accessToken = await decryptConnectionToken(env);
if (!accessToken) return { ok: false, error: "no active Cloudflare OAuth connection" };
const cf: CFApi = getCFApi(env);
const cf: CFApi = await getCFApi(env);
try {
const workerId = await deployAccountWorker(env, account.id, accessToken);
await cf.enableEmailRouting({ accessToken, zoneId: domain.zone_id });