feat(admin): Admin V2 — Ant Design Pro 纯 SPA 重写
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
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
Next.js SSR/hydration 与 SWR fetch-on-mount 存在根本冲突: hydration 卸载组件时 abort 的请求仍占用后端 DB 连接, retry 循环耗尽 PostgreSQL 连接池导致后端完全卡死。 admin-v2 使用 Vite + React + antd 纯 SPA 彻底消除此问题: - 12 页面全部完成(Login, Dashboard, Accounts, Providers, Models, API Keys, Usage, Relay, Config, Prompts, Logs, Agent Templates) - ProTable + ProForm + ProLayout 统一 UI 模式 - TanStack Query + Axios + Zustand 数据层 - JWT 自动刷新 + 401 重试机制 - 全部 18 网络请求 200 OK,零 ERR_ABORTED 同时更新 troubleshooting 第 13 节和 SaaS 平台文档。
This commit is contained in:
89
admin-v2/src/stores/authStore.ts
Normal file
89
admin-v2/src/stores/authStore.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
// ============================================================
|
||||
// ZCLAW Admin V2 — Zustand 认证状态管理
|
||||
// ============================================================
|
||||
|
||||
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', 'relay:admin', 'config:read',
|
||||
'config:write', 'prompt:read', 'prompt:write', 'prompt:publish',
|
||||
],
|
||||
user: ['model:read', 'relay:use', 'config:read', 'prompt:read'],
|
||||
}
|
||||
|
||||
const TOKEN_KEY = 'zclaw_admin_token'
|
||||
const REFRESH_KEY = 'zclaw_admin_refresh_token'
|
||||
const ACCOUNT_KEY = 'zclaw_admin_account'
|
||||
|
||||
function loadFromStorage(): { token: string | null; refreshToken: string | null; account: AccountPublic | null } {
|
||||
const token = localStorage.getItem(TOKEN_KEY)
|
||||
const refreshToken = localStorage.getItem(REFRESH_KEY)
|
||||
const raw = localStorage.getItem(ACCOUNT_KEY)
|
||||
let account: AccountPublic | null = null
|
||||
if (raw) {
|
||||
try { account = JSON.parse(raw) } catch { /* ignore */ }
|
||||
}
|
||||
return { token, refreshToken, account }
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
token: string | null
|
||||
refreshToken: string | null
|
||||
account: AccountPublic | null
|
||||
permissions: string[]
|
||||
|
||||
setToken: (token: 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_PERMISSIONS[stored.account.role] ?? []) : []
|
||||
|
||||
return {
|
||||
token: stored.token,
|
||||
refreshToken: stored.refreshToken,
|
||||
account: stored.account,
|
||||
permissions: perms,
|
||||
|
||||
setToken: (token: string) => {
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
set({ token })
|
||||
},
|
||||
|
||||
login: (token: string, refreshToken: string, account: AccountPublic) => {
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
localStorage.setItem(REFRESH_KEY, refreshToken)
|
||||
localStorage.setItem(ACCOUNT_KEY, JSON.stringify(account))
|
||||
set({
|
||||
token,
|
||||
refreshToken,
|
||||
account,
|
||||
permissions: ROLE_PERMISSIONS[account.role] ?? [],
|
||||
})
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(REFRESH_KEY)
|
||||
localStorage.removeItem(ACCOUNT_KEY)
|
||||
set({ token: null, refreshToken: null, account: null, permissions: [] })
|
||||
},
|
||||
|
||||
hasPermission: (permission: string) => {
|
||||
const { permissions } = get()
|
||||
return permissions.includes(permission) || permissions.includes('admin:full')
|
||||
},
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user