Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor(saas): 重构认证中间件与限流策略
- 登录限流调整为5次/分钟/IP
- 注册限流调整为3次/小时/IP
- GET请求不计入限流
fix(saas): 修复调度器时间戳处理
- 使用NOW()替代文本时间戳
- 兼容TEXT和TIMESTAMPTZ列类型
feat(saas): 实现环境变量插值
- 支持${ENV_VAR}语法解析
- 数据库密码支持环境变量注入
chore: 新增前端管理界面
- 基于React+Ant Design Pro
- 包含路由守卫/错误边界
- 对接58个API端点
docs: 更新安全加固文档
- 新增密钥管理规范
- 记录P0安全项审计结果
- 补充TLS终止说明
test: 完善配置解析单元测试
- 新增环境变量插值测试用例
97 lines
3.2 KiB
TypeScript
97 lines
3.2 KiB
TypeScript
// ============================================================
|
||
// ZCLAW Admin V2 — Zustand 认证状态管理
|
||
// ============================================================
|
||
//
|
||
// 安全策略: JWT token 通过 HttpOnly cookie 传递,前端 JS 无法读取。
|
||
// account 信息(显示名/角色)仍存 localStorage 用于页面刷新后恢复 UI。
|
||
// 内存中的 token/refreshToken 仅用于 Authorization header fallback(API 客户端兼容)。
|
||
|
||
import { create } from 'zustand'
|
||
import type { AccountPublic } from '@/types'
|
||
|
||
/** 权限常量 — 与后端 db.rs SEED_ROLES 保持同步 */
|
||
const ROLE_PERMISSIONS: Record<string, string[]> = {
|
||
super_admin: [
|
||
'admin:full', 'account:admin', 'provider:manage', 'model:manage',
|
||
'relay:admin', 'config:write', 'prompt:read', 'prompt:write',
|
||
'prompt:publish', 'prompt:admin',
|
||
],
|
||
admin: [
|
||
'account:read', 'account:admin', 'provider:manage', 'model:read',
|
||
'model:manage', 'relay:use', 'config:read',
|
||
'config:write', 'prompt:read', 'prompt:write', 'prompt:publish',
|
||
],
|
||
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 } {
|
||
const raw = localStorage.getItem(ACCOUNT_KEY)
|
||
let account: AccountPublic | null = null
|
||
if (raw) {
|
||
try { account = JSON.parse(raw) } catch { /* ignore */ }
|
||
}
|
||
return { account }
|
||
}
|
||
|
||
interface AuthState {
|
||
token: string | null
|
||
refreshToken: string | null
|
||
account: AccountPublic | null
|
||
permissions: string[]
|
||
|
||
setToken: (token: string) => void
|
||
setRefreshToken: (refreshToken: string) => void
|
||
login: (token: string, refreshToken: string, account: AccountPublic) => void
|
||
logout: () => void
|
||
hasPermission: (permission: string) => boolean
|
||
}
|
||
|
||
export const useAuthStore = create<AuthState>((set, get) => {
|
||
const stored = loadFromStorage()
|
||
const perms = stored.account?.role
|
||
? (ROLE_PERMISSIONS[stored.account.role] ?? [])
|
||
: []
|
||
|
||
return {
|
||
token: null,
|
||
refreshToken: null,
|
||
account: stored.account,
|
||
permissions: perms,
|
||
|
||
setToken: (token: string) => {
|
||
set({ token })
|
||
},
|
||
|
||
setRefreshToken: (refreshToken: string) => {
|
||
set({ refreshToken })
|
||
},
|
||
|
||
login: (token: string, refreshToken: string, account: AccountPublic) => {
|
||
// account 保留 localStorage(仅用于 UI 显示,非敏感)
|
||
localStorage.setItem(ACCOUNT_KEY, JSON.stringify(account))
|
||
// token 仅存内存(实际认证通过 HttpOnly cookie)
|
||
set({
|
||
token,
|
||
refreshToken,
|
||
account,
|
||
permissions: ROLE_PERMISSIONS[account.role] ?? [],
|
||
})
|
||
},
|
||
|
||
logout: () => {
|
||
localStorage.removeItem(ACCOUNT_KEY)
|
||
set({ token: null, refreshToken: null, account: null, permissions: [] })
|
||
// 调用后端 logout 清除 HttpOnly cookies(fire-and-forget)
|
||
fetch('/api/v1/auth/logout', { method: 'POST', credentials: 'include' }).catch(() => {})
|
||
},
|
||
|
||
hasPermission: (permission: string) => {
|
||
const { permissions } = get()
|
||
return permissions.includes(permission) || permissions.includes('admin:full')
|
||
},
|
||
}
|
||
})
|