mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(ui,workspace): consume backend access endpoint for capability state
This commit is contained in:
parent
9fc2e5f04f
commit
aa27853712
@ -767,6 +767,7 @@ export const wikiApi = {
|
||||
export const workspaceTeamApi = {
|
||||
list: () => http.get('/workspaces'),
|
||||
get: (id: string | number) => http.get(`/workspaces/${id}`),
|
||||
getAccess: (id: string | number) => http.get(`/workspaces/${id}/access`),
|
||||
create: (data: any) => http.post('/workspaces', data),
|
||||
update: (id: string | number, data: any) => http.put(`/workspaces/${id}`, data),
|
||||
delete: (id: string | number) => http.delete(`/workspaces/${id}`),
|
||||
|
||||
31
mateclaw-ui/src/composables/capabilities.ts
Normal file
31
mateclaw-ui/src/composables/capabilities.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Capability type definitions consumed by route guards and nav filtering.
|
||||
*
|
||||
* Authoritative role -> capability mapping lives on the backend
|
||||
* (`vip.mate.workspace.core.security.RoleCapabilities`). The frontend never
|
||||
* derives the set locally; it only consumes what `/api/v1/workspaces/{id}/access`
|
||||
* returns. Keeping the mapping single-source avoids the "frontend allows but
|
||||
* backend rejects" drift that v1 reviewers flagged.
|
||||
*/
|
||||
|
||||
export type Capability =
|
||||
| 'chat'
|
||||
| 'view:wiki'
|
||||
| 'view:memory'
|
||||
| 'view:dashboard'
|
||||
| 'manage:wiki'
|
||||
| 'manage:agents'
|
||||
| 'manage:skills'
|
||||
| 'manage:channels'
|
||||
| 'manage:models'
|
||||
| 'manage:security'
|
||||
| 'manage:settings'
|
||||
|
||||
export type WorkspaceRole = 'viewer' | 'member' | 'admin' | 'owner'
|
||||
|
||||
export const ROLE_LEVEL: Record<WorkspaceRole, number> = {
|
||||
viewer: 1,
|
||||
member: 2,
|
||||
admin: 3,
|
||||
owner: 4,
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { workspaceTeamApi } from '@/api/index'
|
||||
import type { Capability, WorkspaceRole } from '@/composables/capabilities'
|
||||
import { ROLE_LEVEL } from '@/composables/capabilities'
|
||||
|
||||
export interface Workspace {
|
||||
id: number
|
||||
@ -12,6 +14,12 @@ export interface Workspace {
|
||||
settingsJson?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
/** Real membership role; null for global admins viewing a workspace they have not joined. */
|
||||
memberRole?: WorkspaceRole | null
|
||||
roleLevel?: number
|
||||
isGlobalAdmin?: boolean
|
||||
/** Equals memberRole for normal users; 'owner' for global admins. */
|
||||
effectiveRole?: WorkspaceRole | null
|
||||
}
|
||||
|
||||
export const useWorkspaceStore = defineStore('workspace', () => {
|
||||
@ -21,24 +29,72 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
||||
)
|
||||
const loading = ref(false)
|
||||
|
||||
// RBAC: capabilities for the current workspace, sourced from the backend.
|
||||
// accessLoaded gates router guards so we never render a protected route on a
|
||||
// half-initialized store.
|
||||
const currentCapabilities = ref<Set<Capability>>(new Set())
|
||||
const accessLoaded = ref(false)
|
||||
let accessInFlight: Promise<void> | null = null
|
||||
|
||||
const currentWorkspace = computed(() =>
|
||||
workspaces.value.find((ws) => ws.id === currentWorkspaceId.value) || workspaces.value[0] || null
|
||||
)
|
||||
|
||||
const currentRole = computed<WorkspaceRole | null>(() => {
|
||||
const ws = currentWorkspace.value
|
||||
return (ws?.effectiveRole as WorkspaceRole) || null
|
||||
})
|
||||
|
||||
const isGlobalAdmin = computed(() => Boolean(currentWorkspace.value?.isGlobalAdmin))
|
||||
|
||||
function can(cap: Capability): boolean {
|
||||
return accessLoaded.value && currentCapabilities.value.has(cap)
|
||||
}
|
||||
|
||||
function isAtLeast(role: WorkspaceRole): boolean {
|
||||
if (!currentRole.value) return false
|
||||
return ROLE_LEVEL[currentRole.value] >= ROLE_LEVEL[role]
|
||||
}
|
||||
|
||||
async function refreshAccess() {
|
||||
const id = currentWorkspaceId.value
|
||||
if (id == null) {
|
||||
currentCapabilities.value = new Set()
|
||||
accessLoaded.value = true
|
||||
return
|
||||
}
|
||||
if (accessInFlight) return accessInFlight
|
||||
accessInFlight = (async () => {
|
||||
try {
|
||||
const res: any = await workspaceTeamApi.getAccess(id)
|
||||
const caps: string[] = res?.data?.capabilities || []
|
||||
currentCapabilities.value = new Set(caps as Capability[])
|
||||
} catch (e) {
|
||||
console.warn('Failed to fetch workspace access:', e)
|
||||
currentCapabilities.value = new Set()
|
||||
} finally {
|
||||
accessLoaded.value = true
|
||||
accessInFlight = null
|
||||
}
|
||||
})()
|
||||
return accessInFlight
|
||||
}
|
||||
|
||||
async function fetchWorkspaces() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await workspaceTeamApi.list()
|
||||
workspaces.value = res.data || []
|
||||
// If no workspace selected or selected workspace not in list, default to first
|
||||
if (
|
||||
!currentWorkspaceId.value ||
|
||||
!workspaces.value.find((ws) => ws.id === currentWorkspaceId.value)
|
||||
) {
|
||||
if (workspaces.value.length > 0) {
|
||||
switchWorkspace(workspaces.value[0].id)
|
||||
await switchWorkspace(workspaces.value[0].id)
|
||||
return
|
||||
}
|
||||
}
|
||||
await refreshAccess()
|
||||
} catch (e) {
|
||||
console.warn('Failed to fetch workspaces:', e)
|
||||
} finally {
|
||||
@ -46,17 +102,27 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function switchWorkspace(id: number) {
|
||||
async function switchWorkspace(id: number) {
|
||||
currentWorkspaceId.value = id
|
||||
localStorage.setItem('mc-workspace-id', String(id))
|
||||
accessLoaded.value = false
|
||||
currentCapabilities.value = new Set()
|
||||
await refreshAccess()
|
||||
}
|
||||
|
||||
return {
|
||||
workspaces,
|
||||
currentWorkspaceId,
|
||||
currentWorkspace,
|
||||
currentRole,
|
||||
isGlobalAdmin,
|
||||
currentCapabilities,
|
||||
accessLoaded,
|
||||
loading,
|
||||
can,
|
||||
isAtLeast,
|
||||
fetchWorkspaces,
|
||||
switchWorkspace,
|
||||
refreshAccess,
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user