refactor: v2.2.0 移除本地 sim 模式,Cloudflare OAuth 只走真实授权

- 删除 SimCFApi / WMAILHUB_SIM_CF / isRealOAuth / POST /api/oauth/cloudflare/sim/approve
- 删除前端 OAuthSim 模拟授权页与 /oauth/sim/authorize 路由;账号页「添加账号」整页跳 Cloudflare
- 迁移 0005 删除 cf_sim_resources 表
- 未配置 CF_OAUTH_CLIENT_ID/SECRET 时 start/callback 一律 503(不再降级出假账号假资源)
- 修 OAuth 回调端点:跨站重定向不携带 SameSite=Strict 会话 cookie,改为由一次性 state 承担 CSRF 防护
- 测试改用 tests/cf-mock.ts 在 fetch 层拦截 Cloudflare API,断言真实 HTTP 调用(72/72 通过)
- 本地 dev 无账号时只拉起 Console 并提示人工授权,不再自动造账号
This commit is contained in:
bigcode
2026-09-30 15:33:58 +08:00
parent 067863806f
commit e2c3c353cf
20 changed files with 429 additions and 336 deletions
+3 -4
View File
@@ -153,14 +153,13 @@ export const getAccountWorkerSecret = (id: string) =>
/* ---- OAuth ---- */
export const oauthMode = () => api<{ mode: "cloudflare" | "sim" }>("/api/oauth/mode");
/** OAuth 是否已配置(cloudflare = 可用;unconfigured = 缺少 client id/secret) */
export const oauthMode = () =>
api<{ mode: "cloudflare" | "unconfigured"; authorize_url_base: string | null }>("/api/oauth/mode");
/** 开始授权(自动创建账号记录;授权后自动接入) */
export const oauthStart = () => api<{ url: string; state: string }>("/api/oauth/cloudflare/start");
export const oauthSimApprove = (state: string) =>
api<{ ok: boolean; error?: string; account_id: string }>("/api/oauth/cloudflare/sim/approve", "POST", { state });
/* ---- 域名 ---- */
export interface DomainRow {
-2
View File
@@ -9,7 +9,6 @@ import { ThemeContext } from "./theme-context.ts";
import "./styles.css";
import { App } from "./App.tsx";
import { LoginPage } from "./pages/Login.tsx";
import { OAuthSimPage } from "./pages/OAuthSim.tsx";
import { DashboardPage } from "./pages/Dashboard.tsx";
import { AccountsPage } from "./pages/Accounts.tsx";
import { DomainsPage } from "./pages/Domains.tsx";
@@ -20,7 +19,6 @@ import { setUnauthorizedHandler } from "./api.ts";
/** # 成员变量: 路由表(邮件页路由 /inboxs,v2.1.0) **/
const router = createBrowserRouter([
{ path: "/login", element: <LoginPage /> },
{ path: "/oauth/sim/authorize", element: <OAuthSimPage /> },
{
path: "/",
element: <App />,
+3 -7
View File
@@ -1,6 +1,6 @@
/** 账号页:添加账号直达 Cloudflare OAuth,授权后自动接入并同步域名;搜索、详情、Worker 凭据管理 */
import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useSearchParams } from "react-router-dom";
import {
Button,
Card,
@@ -18,7 +18,6 @@ import {
deleteAccount,
getAccount,
listAccounts,
oauthMode,
oauthStart,
rotateAccount,
updateAccount,
@@ -47,7 +46,6 @@ const STEP_LABEL: Record<string, string> = {
*/
export function AccountsPage() {
const { message, modal } = App.useApp();
const navigate = useNavigate();
const [params] = useSearchParams();
const [items, setItems] = useState<AccountRow[]>([]);
const [loading, setLoading] = useState(false);
@@ -90,13 +88,11 @@ export function AccountsPage() {
[items, q],
);
/** 发起 OAuth:real 模式跳 Cloudflare;sim 模式跳本地模拟授权页 */
/** 发起 OAuth:整页跳转到 Cloudflare 授权页(授权完成后浏览器重定向回 Console 回调端点) */
const startOAuth = async () => {
try {
const mode = (await oauthMode()).mode;
const { url } = await oauthStart();
if (mode === "cloudflare") window.location.href = url;
else navigate(url); // /oauth/sim/authorize?state=...
window.location.href = url;
} catch (err) {
message.error(err instanceof Error ? err.message : "发起授权失败");
}
-55
View File
@@ -1,55 +0,0 @@
/** sim 模式的 Cloudflare 授权模拟页:确认后调用 approve,再跳回节点页 */
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { Button, Card, Space, App } from "antd";
import { oauthSimApprove } from "../api.ts";
/** # 函数
* 模拟授权页(仅本地 sim 模式;真实部署直接跳 Cloudflare 授权页)
*/
export function OAuthSimPage() {
const [params] = useSearchParams();
const navigate = useNavigate();
const { message } = App.useApp();
const state = params.get("state") ?? "";
const [busy, setBusy] = useState(false);
const approve = async () => {
setBusy(true);
try {
const res = await oauthSimApprove(state);
if (res.ok) {
message.success("Cloudflare 授权与自动安装完成");
navigate(`/accounts?provision=${encodeURIComponent(res.account_id)}`, { replace: true });
} else {
message.error(res.error ?? "安装失败");
navigate("/accounts", { replace: true });
}
} catch (err) {
message.error(err instanceof Error ? err.message : "授权失败");
navigate("/accounts", { replace: true });
} finally {
setBusy(false);
}
};
// state 缺失直接回节点页
useEffect(() => {
if (!state) navigate("/accounts", { replace: true });
}, [state, navigate]);
return (
<div className="login-page">
<Card className="login-card" title="Cloudflare 授权(本地模拟)" styles={{ header: { textAlign: "center" } }}>
<p>sim 模式下,该页面模拟 Cloudflare OAuth 授权同意流程。</p>
<p className="muted">真实部署时,这里会跳转到 dash.cloudflare.com 的授权页面。</p>
<Space style={{ width: "100%", justifyContent: "center", marginTop: 16 }}>
<Button onClick={() => navigate("/accounts")}>拒绝</Button>
<Button type="primary" loading={busy} onClick={() => void approve()}>
同意授权
</Button>
</Space>
</Card>
</div>
);
}