From 0fd8489c222ae24e0e8640ad63922f1a421a051e Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Sun, 14 Jun 2026 11:15:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E7=99=BB=E5=87=BA=E7=95=99?= =?UTF-8?q?=E7=97=95=E5=AE=A1=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 原登出为纯前端清 token,不发请求,故无记录 - 新增 POST /api/auth/logout 端点(无状态,仅供审计留痕),deriveActionLabel 加「登出」标签 - 前端 logout 先带 token 通知后端再清本地,失败不阻塞 - 审计中间件经 authMiddleware 解析操作人,记录谁/何时登出 --- src/server/index.ts | 10 ++++++++++ web/src/stores/authStore.ts | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/src/server/index.ts b/src/server/index.ts index ef4cf4d..a2ae868 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -150,6 +150,7 @@ function deriveActionLabel(method: string, path: string): string { const p = path.replace(/\/+$/, ''); const rules: ReadonlyArray<[RegExp, string]> = [ [/^\/api\/auth\/login$/, '登录'], + [/^\/api\/auth\/logout$/, '登出'], [/^\/api\/assessments\/run$/, '运行评估(创建/重评)'], [/^\/api\/assessments\/[^/]+\/submit$/, '申报报送风控'], [/^\/api\/assessments\/[^/]+\/resubmit$/, '驳回后重新提交'], @@ -319,6 +320,15 @@ app.post('/api/auth/login', async (c) => { return c.json({ token, username, role }); }); +/** + * POST /api/auth/logout + * 登出:无状态 JWT 由客户端丢弃 token 即可,本端点仅用于留痕审计(记录谁/何时登出)。 + * 需携带有效 token,审计中间件据此解析操作人。 + */ +app.post('/api/auth/logout', (c) => { + return c.json({ ok: true }); +}); + /** LLM 启用状态(不泄露密钥),供前端/调试查询。 */ app.get('/api/llm/status', (c) => { const cfg = getLlmConfig(); diff --git a/web/src/stores/authStore.ts b/web/src/stores/authStore.ts index 2edbc80..2dd3bf0 100644 --- a/web/src/stores/authStore.ts +++ b/web/src/stores/authStore.ts @@ -95,6 +95,14 @@ export const useAuthStore = create((set) => ({ }, logout: () => { + // 先通知后端留痕(登出审计),再清除本地令牌;失败不阻塞登出。 + try { + const token = localStorage.getItem('risk-agent-token'); + void fetch(`${API_BASE}/api/auth/logout`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, + }).catch(() => undefined); + } catch { /* ignore */ } saveToStorage(null); try { localStorage.removeItem('risk-agent-token'); localStorage.removeItem('risk-agent-uid'); } catch { /* ignore */ } set({ isAuthenticated: false, user: null, error: null });