security: 安全审计修复 - API Key清理 + JWT改为HttpOnly Cookie
- API Key: run.md/.env/seed_model_providers.sql 中明文Key替换为占位符 - 认证: JWT从localStorage迁移到HttpOnly Cookie,移除后端body返回token - 前端: api.ts/knowledge/page.tsx/auth.ts 全面改用Cookie认证 - postcss XSS: package.json添加overrides强制升级到8.5.15 - gitignore: 添加server/.env和.env排除规则 - lint: 移除tenant.go中未使用的roleKey常量 - 文档: 新增docs/security-audit-report.md和hardware-requirements.md
This commit is contained in:
@@ -19,8 +19,6 @@ setup("登录法制日报账号并保存会话", async ({ page }) => {
|
||||
|
||||
await page.waitForURL(/\/store/, { timeout: 30_000 });
|
||||
|
||||
const token = await page.evaluate(() => localStorage.getItem("token"));
|
||||
expect(token, "登录后应在 localStorage 写入 token").toBeTruthy();
|
||||
|
||||
// HttpOnly Cookie: 登录后 token 存在 cookie 中,通过 storageState 自动捕获
|
||||
await page.context().storageState({ path: AUTH_FILE });
|
||||
});
|
||||
Generated
+4
-32
@@ -8946,34 +8946,6 @@
|
||||
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/next/node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
@@ -9544,9 +9516,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -9563,7 +9535,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
|
||||
@@ -46,5 +46,8 @@
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5",
|
||||
"typescript-eslint": "^8.62.0"
|
||||
},
|
||||
"overrides": {
|
||||
"postcss": ">=8.5.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,10 +45,9 @@ export default function RegisterPage() {
|
||||
email: string;
|
||||
role: "user" | "super_admin" | "admin" | "creator";
|
||||
};
|
||||
access_token: string;
|
||||
}>("/api/v1/auth/register", { name, email, password });
|
||||
|
||||
setAuth(res.user, res.access_token);
|
||||
setAuth(res.user);
|
||||
toast.success("注册成功");
|
||||
router.push("/store");
|
||||
} catch (err) {
|
||||
|
||||
@@ -113,8 +113,8 @@ export default function KnowledgePage() {
|
||||
formData.append("file", file);
|
||||
const res = await fetch(`/api/v1/knowledge/${selectedKB!.id}/documents`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
||||
body: formData,
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) throw new Error("上传失败");
|
||||
return res.json();
|
||||
|
||||
+1
-10
@@ -36,11 +36,8 @@ class APIError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
// 认证:token 通过 HttpOnly Cookie 自动携带,不再使用 localStorage
|
||||
function getAuthHeaders(): Record<string, string> {
|
||||
if (typeof window !== "undefined") {
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) return { Authorization: `Bearer ${token}` };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -81,12 +78,6 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
if (typeof window !== "undefined") {
|
||||
const currentPath = window.location.pathname;
|
||||
if (currentPath !== "/login" && currentPath !== "/register") {
|
||||
localStorage.removeItem("token");
|
||||
}
|
||||
}
|
||||
throw new APIError(json.code || 40101, json.message || "未登录或登录已过期");
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ interface AuthState {
|
||||
fetchUser: () => Promise<void>;
|
||||
login: (email: string, password: string, orgId?: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
setAuth: (user: User, token: string) => void;
|
||||
setAuth: (user: User) => void;
|
||||
switchOrg: (orgId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -26,12 +26,6 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
if (get()._hasFetched) return;
|
||||
set({ _hasFetched: true });
|
||||
|
||||
const token = typeof window !== "undefined" ? localStorage.getItem("token") : null;
|
||||
if (!token) {
|
||||
set({ user: null, isAuthenticated: false, isLoading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
@@ -39,7 +33,6 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
const user = await api.get<User>("/api/v1/auth/me", { signal: controller.signal });
|
||||
set({ user, isAuthenticated: true, isLoading: false });
|
||||
} catch {
|
||||
if (typeof window !== "undefined") localStorage.removeItem("token");
|
||||
set({ user: null, isAuthenticated: false, isLoading: false });
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
@@ -47,19 +40,15 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
},
|
||||
|
||||
login: async (email: string, password: string, orgId?: string) => {
|
||||
const data = await api.post<{ user: User; access_token: string }>("/api/v1/auth/login", {
|
||||
const data = await api.post<{ user: User }>("/api/v1/auth/login", {
|
||||
email,
|
||||
password,
|
||||
org_id: orgId,
|
||||
});
|
||||
if (data.access_token) {
|
||||
localStorage.setItem("token", data.access_token);
|
||||
}
|
||||
set({ user: data.user, isAuthenticated: true, isLoading: false });
|
||||
},
|
||||
|
||||
setAuth: (user: User, token: string) => {
|
||||
if (token) localStorage.setItem("token", token);
|
||||
setAuth: (user: User) => {
|
||||
set({ user, isAuthenticated: true, isLoading: false });
|
||||
},
|
||||
|
||||
@@ -69,7 +58,6 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
localStorage.removeItem("token");
|
||||
set({ user: null, isAuthenticated: false, _hasFetched: false });
|
||||
}
|
||||
},
|
||||
@@ -78,12 +66,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
const data = await api.post<{
|
||||
message: string;
|
||||
org: Organization;
|
||||
token?: string;
|
||||
user?: User;
|
||||
}>("/api/v1/auth/switch-org", { org_id: orgId });
|
||||
if (data.token) {
|
||||
localStorage.setItem("token", data.token);
|
||||
}
|
||||
if (data.user && data.org) {
|
||||
set({ user: { ...data.user, org_id: orgId, org: data.org }, isAuthenticated: true });
|
||||
} else {
|
||||
@@ -93,4 +77,4 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
}));
|
||||
Reference in New Issue
Block a user