From 09310cb753192adea6f74dc73f3b9bf3a3ff64d0 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 11 Oct 2026 13:53:27 +0000 Subject: [PATCH] Clear stale API-key credentials when a password session begins Why: the auth middleware prefers Bearer keys over the session cookie, so a stale key left in localStorage shadows valid cookie sessions and every request after password login returns 401 (observed at http://192.168.3.2:8788 on 2026-10-11). Evidence: modules/frontend vitest 19 files / 88 tests green; auth.test.ts covers beginSession clearing the stored Bearer before any fetch. Impact: password logins switch to the HttpOnly cookie session cleanly; API-key logins unchanged. Refs: task card feature-login-clear-stale-key in Agent-System-Dev. --- src/auth.ts | 3 +++ src/components/LoginPage.tsx | 2 ++ test/auth.test.ts | 11 +++++++++++ 3 files changed, 16 insertions(+) diff --git a/src/auth.ts b/src/auth.ts index cbfe5e8..993728e 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -22,6 +22,9 @@ export const auth = { apiKey(): string { return storage.getItem(KEY) ?? '' }, setKey(key: string) { storage.setItem(KEY, key.trim()) }, clear() { storage.removeItem(KEY) }, + // 密码登录成功后切换到 HttpOnly 会话 cookie:必须清掉残留 Bearer——服务端 + // 认证中间件 Bearer 优先于会话 cookie,失效残留 key 会把有效会话也判成 401。 + beginSession() { this.clear() }, // 带凭证的 fetch:未设置密钥时与原生 fetch 等价(嵌入式档无认证)。 fetch(input: string, init?: RequestInit): Promise { const key = this.apiKey() diff --git a/src/components/LoginPage.tsx b/src/components/LoginPage.tsx index e04e78f..4dac605 100644 --- a/src/components/LoginPage.tsx +++ b/src/components/LoginPage.tsx @@ -41,6 +41,8 @@ export function LoginPage({ onAuthed, onMustChange }: { onAuthed: () => void; on setErr(''); setBusy(true) try { const me = await api.login(owner.trim(), password) + // 会话凭证是 HttpOnly cookie;清残留 Bearer,避免其遮蔽有效会话。 + auth.beginSession() if (me.mustChangePassword) { // 初始/重置密码:先完成强制改密再放行。 onMustChange(me.owner) diff --git a/test/auth.test.ts b/test/auth.test.ts index 112c9ea..4491492 100644 --- a/test/auth.test.ts +++ b/test/auth.test.ts @@ -40,4 +40,15 @@ describe('auth 凭证模块', () => { expect(auth.apiKey()).toBe('') expect(auth.withKey('/x')).toBe('/x') }) + + it('beginSession:密码会话生效后清残留 Bearer,fetch 不再带头', async () => { + auth.setKey('ask_stale') + auth.beginSession() + expect(auth.apiKey()).toBe('') + const spy = vi.fn().mockResolvedValue(new Response('{}')) + vi.stubGlobal('fetch', spy) + await auth.fetch('/api/me') + const [, init] = spy.mock.calls[0] as [string, RequestInit | undefined] + expect(new Headers(init?.headers ?? {}).get('Authorization')).toBeNull() + }) }) -- 2.54.0