feat(auth): 鉴权片2 —— 前端登录闭环 + 保护路由 + 去掉 header 兜底
把 JWT 鉴权从后端核心闭环到端到端: 后端: - middleware.RequireAuth:上下文无已验证 uid 则 401;挂在 owner 作用域业务路由组。 - 路由拆 公开/受保护:公开=auth/health + 按 task_id 寻址的 SSE 与报告导出 (EventSource/下载无法带 Bearer);受保护=tasks/memory/kb*/agents/reports/billing。 - userID(c) 去掉 X-User-ID 兜底,仅信任 JWT 注入的 uid。 - 修 CORS:Allow-Headers 增 Authorization(否则浏览器拦截带 Bearer 的请求)。 前端: - lib/api:token 存 localStorage + Bearer 头(不再发 X-User-ID)+ authRegister/Login/Me + 401 清令牌并广播 sdx:logout;submitTask/report/memory/列表加载走 Bearer 与 401 守卫。 - views/Login:登录/注册全屏门。 - App:启动校验令牌 → 无则渲染 Login,有则进主应用;identity.userId=已验证 user.id; 监听 sdx:logout 回登录页。 - TopBar:去掉可编辑身份输入,改显登录用户 + 登出。 实跑验证(docker+gateway+preview): - RequireAuth:无 token /kb/list、/agents → 401;/health → 200;带 token → 200。 - 前端:无 token 显登录门;注入有效 token 重载 → 进主应用、顶栏显 Dexter、KB 加载本人库、 隔离徽标显雪花 uid。控制台无错、生产构建通过。 - 过程中发现并修复 CORS 缺 Authorization 头的真实 bug。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,11 +12,22 @@ import { RunsView } from "./views/RunsView";
|
||||
import { Home } from "./views/Home";
|
||||
import { Placeholder } from "./views/Placeholder";
|
||||
import { CommandPalette, type Command } from "./components/CommandPalette";
|
||||
import { submitTask, streamTokens, streamExec, type Identity } from "./lib/api";
|
||||
import { Login } from "./views/Login";
|
||||
import { submitTask, streamTokens, streamExec, authMe, logout, type Identity, type AuthUser } from "./lib/api";
|
||||
import type { TaskDsl } from "./lib/dsl";
|
||||
import { emptyRun, type RunState } from "./lib/run";
|
||||
import { ToastProvider } from "./ui";
|
||||
|
||||
// 会话标识:本地持久化生成一次(多轮历史用,与鉴权身份分离)。
|
||||
function getSessionId(): string {
|
||||
let s = localStorage.getItem("sdx_sess");
|
||||
if (!s) {
|
||||
s = "sess-" + Math.random().toString(36).slice(2, 10);
|
||||
localStorage.setItem("sdx_sess", s);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
const PLACEHOLDERS: Partial<Record<ViewKey, { title: string; desc: string }>> = {
|
||||
home: { title: "工作台", desc: "概览:知识库 / 文档 / 近期运行 / 待办报告 / 配额计费 + 快捷入口。" },
|
||||
kb: { title: "知识库 (RAG)", desc: "入库流水线监控 · 检索调试台(带来源徽标) · 文档/块浏览 · 知识图谱 · 检索评测。依赖 embedding + 入库 worker + 真实混合检索。" },
|
||||
@@ -28,7 +39,9 @@ const PLACEHOLDERS: Partial<Record<ViewKey, { title: string; desc: string }>> =
|
||||
|
||||
export default function App() {
|
||||
const [view, setView] = useState<ViewKey>("home");
|
||||
const [identity, setIdentity] = useState<Identity>({ userId: "wt", sessionId: "sess-ui" });
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [authLoading, setAuthLoading] = useState(true);
|
||||
const identity = useMemo<Identity>(() => ({ userId: user?.id ?? "", sessionId: getSessionId() }), [user]);
|
||||
const [run, setRun] = useState<RunState>(emptyRun);
|
||||
const [cmdOpen, setCmdOpen] = useState(false);
|
||||
const closeRef = useRef<(() => void) | null>(null);
|
||||
@@ -47,6 +60,22 @@ export default function App() {
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, []);
|
||||
|
||||
// 启动校验现有令牌;并监听 401 登出事件(令牌失效 → 回登录页)。
|
||||
useEffect(() => {
|
||||
authMe()
|
||||
.then(setUser)
|
||||
.catch(() => setUser(null))
|
||||
.finally(() => setAuthLoading(false));
|
||||
const onLogout = () => setUser(null);
|
||||
window.addEventListener("sdx:logout", onLogout);
|
||||
return () => window.removeEventListener("sdx:logout", onLogout);
|
||||
}, []);
|
||||
|
||||
const onLogout = useCallback(() => {
|
||||
logout();
|
||||
setUser(null);
|
||||
}, []);
|
||||
|
||||
const commands = useMemo<Command[]>(() => {
|
||||
const go = (key: ViewKey) => () => setView(key);
|
||||
return [
|
||||
@@ -109,6 +138,18 @@ export default function App() {
|
||||
[identity],
|
||||
);
|
||||
|
||||
// 鉴权门:校验中显示占位;未登录显示登录页;登录后进入主应用。
|
||||
if (authLoading) {
|
||||
return <div className="flex h-screen w-screen items-center justify-center bg-ink-950 text-sm text-slate-500">加载中…</div>;
|
||||
}
|
||||
if (!user) {
|
||||
return (
|
||||
<ToastProvider>
|
||||
<Login onAuthed={setUser} />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
<div className="relative flex h-screen w-screen flex-col bg-ink-950 text-slate-200">
|
||||
@@ -117,7 +158,7 @@ export default function App() {
|
||||
className="pointer-events-none absolute inset-x-0 top-0 h-64 opacity-60"
|
||||
style={{ background: "radial-gradient(60% 100% at 50% 0%, rgba(124,92,246,0.10), transparent 70%)" }}
|
||||
/>
|
||||
<TopBar identity={identity} setIdentity={setIdentity} onCommand={() => setCmdOpen(true)} />
|
||||
<TopBar user={user} onLogout={onLogout} onCommand={() => setCmdOpen(true)} />
|
||||
<div className="relative flex min-h-0 flex-1">
|
||||
<LeftNav active={view} onSelect={setView} />
|
||||
<main className="min-w-0 flex-1 overflow-hidden">
|
||||
|
||||
Reference in New Issue
Block a user