// ============================================================ // ZCLAW Admin V2 — Zustand 认证状态管理 // ============================================================ // // 安全策略: JWT token 通过 HttpOnly cookie 传递,前端 JS 无法读取。 // account 信息(显示名/角色)存 localStorage 用于页面刷新后恢复 UI。 // isAuthenticated 标记用于判断登录状态,不暴露任何 token 到 JS。 import { create } from 'zustand' import type { AccountPublic } from '@/types' /** 权限常量 — 与后端 db.rs seed_roles 保持同步 */ const ROLE_PERMISSIONS: Record = { super_admin: [ 'admin:full', 'account:admin', 'provider:manage', 'model:manage', 'model:read', 'relay:admin', 'relay:use', 'config:write', 'config:read', 'prompt:read', 'prompt:write', 'prompt:publish', 'prompt:admin', 'scheduler:read', 'knowledge:read', 'knowledge:write', 'billing:read', 'billing:write', ], admin: [ 'account:read', 'account:admin', 'provider:manage', 'model:read', 'model:manage', 'relay:use', 'relay:admin', 'config:read', 'config:write', 'prompt:read', 'prompt:write', 'prompt:publish', 'scheduler:read', 'knowledge:read', 'knowledge:write', 'billing:read', ], user: ['model:read', 'relay:use', 'config:read', 'prompt:read'], } const ACCOUNT_KEY = 'zclaw_admin_account' /** 从 localStorage 恢复 account 信息(token 通过 HttpOnly cookie 管理) */ function loadFromStorage(): { account: AccountPublic | null; isAuthenticated: boolean } { const raw = localStorage.getItem(ACCOUNT_KEY) let account: AccountPublic | null = null if (raw) { try { account = JSON.parse(raw) } catch { /* ignore */ } } // IMPORTANT: Do NOT set isAuthenticated = true from localStorage alone. // The HttpOnly cookie must be validated via GET /auth/me before we trust // the session. This prevents the AuthGuard race condition where children // render and make API calls with an expired cookie. return { account, isAuthenticated: false } } interface AuthState { isAuthenticated: boolean account: AccountPublic | null permissions: string[] login: (account: AccountPublic) => void logout: () => void hasPermission: (permission: string) => boolean } export const useAuthStore = create((set, get) => { const stored = loadFromStorage() const perms = stored.account?.role ? (ROLE_PERMISSIONS[stored.account.role] ?? []) : [] return { isAuthenticated: stored.isAuthenticated, account: stored.account, permissions: perms, login: (account: AccountPublic) => { // account 保留 localStorage(仅用于 UI 显示,非敏感) localStorage.setItem(ACCOUNT_KEY, JSON.stringify(account)) set({ isAuthenticated: true, account, permissions: ROLE_PERMISSIONS[account.role] ?? [], }) }, logout: () => { localStorage.removeItem(ACCOUNT_KEY) set({ isAuthenticated: false, account: null, permissions: [] }) // 调用后端 logout 清除 HttpOnly cookies(fire-and-forget) fetch(`${import.meta.env.VITE_API_BASE_URL || '/api/v1'}/auth/logout`, { method: 'POST', credentials: 'include' }).catch(() => {}) }, hasPermission: (permission: string) => { const { permissions } = get() return permissions.includes(permission) || permissions.includes('admin:full') }, } })