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:
@@ -0,0 +1,72 @@
|
||||
import { useState } from "react";
|
||||
import { LogIn, UserPlus, Loader2 } from "lucide-react";
|
||||
import { authLogin, authRegister, type AuthUser } from "../lib/api";
|
||||
import { Button, Input, Field } from "../ui";
|
||||
|
||||
// Login:未登录时的全屏鉴权门。登录/注册成功后回调 onAuthed 把用户交给 App。
|
||||
export function Login({ onAuthed }: { onAuthed: (u: AuthUser) => void }) {
|
||||
const [mode, setMode] = useState<"login" | "register">("login");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
const submit = async () => {
|
||||
if (busy) return;
|
||||
setErr("");
|
||||
setBusy(true);
|
||||
try {
|
||||
const u = mode === "login" ? await authLogin(email.trim(), password) : await authRegister(email.trim(), password, name.trim());
|
||||
onAuthed(u);
|
||||
} catch (e) {
|
||||
setErr((e as Error).message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isRegister = mode === "register";
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-ink-950 text-slate-200">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 top-0 h-80 opacity-60"
|
||||
style={{ background: "radial-gradient(60% 100% at 50% 0%, rgba(124,92,246,0.12), transparent 70%)" }}
|
||||
/>
|
||||
<div className="relative w-[360px] rounded-xl border border-line bg-ink-900 p-6 shadow-card">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-brand/20 text-brand-400 font-bold">S</div>
|
||||
<span className="text-lg font-semibold text-slate-100">sundynix-agentix</span>
|
||||
</div>
|
||||
<p className="mb-5 text-xs text-slate-500">{isRegister ? "创建账户以开始" : "登录以继续"}</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
{isRegister && (
|
||||
<Field label="昵称">
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} placeholder="如 Dexter" autoFocus={isRegister} />
|
||||
</Field>
|
||||
)}
|
||||
<Field label="邮箱">
|
||||
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@example.com" autoFocus={!isRegister} onKeyDown={(e) => e.key === "Enter" && submit()} />
|
||||
</Field>
|
||||
<Field label="密码">
|
||||
<Input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder={isRegister ? "至少 6 位" : "••••••••"} onKeyDown={(e) => e.key === "Enter" && submit()} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{err && <div className="mt-3 rounded-md bg-danger/10 px-3 py-2 text-xs text-danger">{err}</div>}
|
||||
|
||||
<Button variant="primary" className="mt-4 w-full justify-center" icon={busy ? Loader2 : isRegister ? UserPlus : LogIn} disabled={busy || !email.trim() || !password} onClick={submit}>
|
||||
{busy ? "处理中…" : isRegister ? "注册并进入" : "登录"}
|
||||
</Button>
|
||||
|
||||
<div className="mt-3 text-center text-[11px] text-slate-500">
|
||||
{isRegister ? "已有账户?" : "还没有账户?"}
|
||||
<button className="ml-1 text-brand-400 hover:underline" onClick={() => { setErr(""); setMode(isRegister ? "login" : "register"); }}>
|
||||
{isRegister ? "去登录" : "去注册"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user