Files
Agent-System-Frontend/src/auth.ts
T
weichen 09310cb753 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.
2026-10-11 13:53:27 +00:00

48 lines
2.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// API key 凭证模块:登录门存取(localStorage 优先,无 DOM 环境回退内存)。
// 服务端开启认证时(PG 档默认),所有请求带 Bearer 头;SSE/静态资源链接走 ?key=。
const KEY = 'agent-system.apiKey'
interface KV { getItem(k: string): string | null; setItem(k: string, v: string): void; removeItem(k: string): void }
function pickStorage(): KV {
try {
if (typeof localStorage !== 'undefined') return localStorage
} catch { /* 隐私模式等 */ }
const mem = new Map<string, string>()
return {
getItem: k => mem.get(k) ?? null,
setItem: (k, v) => void mem.set(k, v),
removeItem: k => void mem.delete(k),
}
}
const storage = pickStorage()
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<Response> {
const key = this.apiKey()
if (!key) return fetch(input, init)
const headers = new Headers(init?.headers ?? {})
headers.set('Authorization', `Bearer ${key}`)
return fetch(input, { ...init, headers })
},
// 退出登录:清凭证并整页回登录门(服务端会话/密钥均为无状态凭证,无需服务端注销)。
logout() {
this.clear()
try { location.reload() } catch { /* 非 DOM 环境(测试)忽略 */ }
},
// 无法带头的资源 URL(EventSource / <img> / 下载):追加 ?key=。
withKey(url: string): string {
const key = this.apiKey()
if (!key) return url
return url + (url.includes('?') ? '&' : '?') + 'key=' + encodeURIComponent(key)
},
}