feat(config,gateway,web): 路径式安全入口 ADMIN_PATH + 数据目录 platform 哨兵(裁决 1.6/16.4.7/12.4.2 + 1.5/12.4.3 核对)
- 1.6 + 16.4.7:安全入口=路径式 `/<ADMIN_PATH>`(env `ADMIN_PATH`,默认 `admin`;`xxx/admin` 亦可,
非法值回退 `/admin`)。命中前缀 → 送注入了 `<base href="/">` + `window.__CYRENE_ADMIN_ENTRY__` 的面板入口页
(深链直达);**形如面板入口但前缀不符 → 404**(不回落用户端聊天壳);`/api/*`、`/ws`、webhook 先放行
⇒ **API 不藏**。前端路由改**路径式**(`router.ts` 只读注入,无硬编兜底;移除 hash 入口)
- 1.5 / 12.4.3 核对:暴露面默认 `0.0.0.0` 与裁决一致,无需改代码(仅补注释)
- 12.4.2:`DATA_DIR` 支持 `platform` 哨兵(大小写不敏感)→ 平台标准目录;默认 `./data` 与显式空值行为不变
(显式空值保持原兜底,避免正在运行实例突然换库)
- 复核:build/vet 绿 + config/gateway 包测试绿 + linux/darwin 交叉编译绿;新增 7 条用例,
并用真实构建产物(`webui.Assets()`)跑通「浏览器直开 /admin 能启动面板」(`<base>` 必须在资源引用之前)
- 变异 2 条(关掉错前缀 404 / 前端默认前缀兜底)已还原
- ⚠️ 运行时序:前端已生效而后端 hatch 未生效期间**面板打不开** ⇒ 需紧接一次「build 到位 + restart」
This commit is contained in:
1 parent
9253fef92a
commit
a398dc9b92
7 files changed
+481
-33
No files matched your search
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { TopBar } from "./components/TopBar";
|
||||
import { Sidebar, MobileNav } from "./components/Nav";
|
||||
import { ModeRouter } from "./modes/ModeRouter";
|
||||
import { AdminApp } from "./admin/AdminApp";
|
||||
import { Login } from "./components/Login";
|
||||
import { parseAdminHash } from "./admin/router";
|
||||
import { parseAdminRoute, readAdminEntry, subscribeAdminRoute } from "./admin/router";
|
||||
import { connectWs, disconnectWs } from "./lib/ws";
|
||||
import { markSessionOpenedRead } from "./lib/sessionRead";
|
||||
import { initTheme } from "./lib/theme";
|
||||
@@ -21,7 +21,15 @@ import { personaMoodSeed } from "./store/personaSelectors";
|
||||
export default function App(): React.ReactNode {
|
||||
// Apply the persisted (dark-first) theme and open the realtime channel only
|
||||
// when we hold a token — otherwise the gateway rejects the handshake with 401.
|
||||
const [route, setRoute] = useState(() => parseAdminHash(window.location.hash));
|
||||
//
|
||||
// 管理面板是**安全入口**(裁决 1.6 / 16.4.7,design/07 §0.2):入口前缀由后端
|
||||
// 按 .env 的 ADMIN_PATH 注入到面板那一份 index.html
|
||||
// (`window.__CYRENE_ADMIN_ENTRY__`,见 internal/services/gateway/routes.go)。
|
||||
// 用户端页面没有这个全局量,因此这里的 route.admin 恒为 false —— 用户端
|
||||
// 既不含入口链接、也解析不出面板路由。面板模块切换走 history(不再读 hash,
|
||||
// hash 只留给渠道页的子页)。
|
||||
const entry = useMemo(readAdminEntry, []);
|
||||
const [route, setRoute] = useState(() => parseAdminRoute(window.location.pathname, entry));
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const setPersonas = useAppStore((s) => s.setPersonas);
|
||||
const seedMoods = useAppStore((s) => s.seedMoods);
|
||||
@@ -29,13 +37,13 @@ export default function App(): React.ReactNode {
|
||||
useEffect(() => {
|
||||
initTheme();
|
||||
if (getAuthToken()) connectWs();
|
||||
const onHash = () => setRoute(parseAdminHash(window.location.hash));
|
||||
window.addEventListener("hashchange", onHash);
|
||||
const onRoute = () => setRoute(parseAdminRoute(window.location.pathname, entry));
|
||||
const unsubscribe = subscribeAdminRoute(onRoute);
|
||||
return () => {
|
||||
disconnectWs();
|
||||
window.removeEventListener("hashchange", onHash);
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
}, [entry]);
|
||||
|
||||
// Load the role catalog (design/22 §4.6). The rendered role is no longer one
|
||||
// global value: it is derived per active session from this catalog plus that
|
||||
@@ -136,7 +144,7 @@ export default function App(): React.ReactNode {
|
||||
}, [activeSessionId, activeSessionKnown, setMessages]);
|
||||
|
||||
// The M9 modal UI and the admin subsystem (design/10) are two separate shells.
|
||||
// `/admin` is reached via `#/admin`; every admin module is a hash segment.
|
||||
// 面板由安全入口路径进入(见上方 route 注释),每个模块是前缀下的一段路径。
|
||||
if (route.admin) {
|
||||
return <AdminApp section={route.section} />;
|
||||
}
|
||||
@@ -144,7 +152,7 @@ export default function App(): React.ReactNode {
|
||||
// No credential: never render the chat shell (it would silently show an
|
||||
// "未登录" shell with no way in). Show the user-side login card instead; its
|
||||
// onSuccess reloads the page so connectWs + session/persona effects re-run
|
||||
// with the fresh token. Admin shell keeps its own LoginGate (#/admin).
|
||||
// with the fresh token. Admin shell keeps its own LoginGate.
|
||||
if (!getAuthToken()) {
|
||||
return <Login onSuccess={() => window.location.reload()} />;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user