mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +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 = {
|
export const workspaceTeamApi = {
|
||||||
list: () => http.get('/workspaces'),
|
list: () => http.get('/workspaces'),
|
||||||
get: (id: string | number) => http.get(`/workspaces/${id}`),
|
get: (id: string | number) => http.get(`/workspaces/${id}`),
|
||||||
|
getAccess: (id: string | number) => http.get(`/workspaces/${id}/access`),
|
||||||
create: (data: any) => http.post('/workspaces', data),
|
create: (data: any) => http.post('/workspaces', data),
|
||||||
update: (id: string | number, data: any) => http.put(`/workspaces/${id}`, data),
|
update: (id: string | number, data: any) => http.put(`/workspaces/${id}`, data),
|
||||||
delete: (id: string | number) => http.delete(`/workspaces/${id}`),
|
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 { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { workspaceTeamApi } from '@/api/index'
|
import { workspaceTeamApi } from '@/api/index'
|
||||||
|
import type { Capability, WorkspaceRole } from '@/composables/capabilities'
|
||||||
|
import { ROLE_LEVEL } from '@/composables/capabilities'
|
||||||
|
|
||||||
export interface Workspace {
|
export interface Workspace {
|
||||||
id: number
|
id: number
|
||||||
@ -12,6 +14,12 @@ export interface Workspace {
|
|||||||
settingsJson?: string
|
settingsJson?: string
|
||||||
createTime?: string
|
createTime?: string
|
||||||
updateTime?: 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', () => {
|
export const useWorkspaceStore = defineStore('workspace', () => {
|
||||||
@ -21,24 +29,72 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
|||||||
)
|
)
|
||||||
const loading = ref(false)
|
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(() =>
|
const currentWorkspace = computed(() =>
|
||||||
workspaces.value.find((ws) => ws.id === currentWorkspaceId.value) || workspaces.value[0] || null
|
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() {
|
async function fetchWorkspaces() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res: any = await workspaceTeamApi.list()
|
const res: any = await workspaceTeamApi.list()
|
||||||
workspaces.value = res.data || []
|
workspaces.value = res.data || []
|
||||||
// If no workspace selected or selected workspace not in list, default to first
|
|
||||||
if (
|
if (
|
||||||
!currentWorkspaceId.value ||
|
!currentWorkspaceId.value ||
|
||||||
!workspaces.value.find((ws) => ws.id === currentWorkspaceId.value)
|
!workspaces.value.find((ws) => ws.id === currentWorkspaceId.value)
|
||||||
) {
|
) {
|
||||||
if (workspaces.value.length > 0) {
|
if (workspaces.value.length > 0) {
|
||||||
switchWorkspace(workspaces.value[0].id)
|
await switchWorkspace(workspaces.value[0].id)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await refreshAccess()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to fetch workspaces:', e)
|
console.warn('Failed to fetch workspaces:', e)
|
||||||
} finally {
|
} finally {
|
||||||
@ -46,17 +102,27 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchWorkspace(id: number) {
|
async function switchWorkspace(id: number) {
|
||||||
currentWorkspaceId.value = id
|
currentWorkspaceId.value = id
|
||||||
localStorage.setItem('mc-workspace-id', String(id))
|
localStorage.setItem('mc-workspace-id', String(id))
|
||||||
|
accessLoaded.value = false
|
||||||
|
currentCapabilities.value = new Set()
|
||||||
|
await refreshAccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workspaces,
|
workspaces,
|
||||||
currentWorkspaceId,
|
currentWorkspaceId,
|
||||||
currentWorkspace,
|
currentWorkspace,
|
||||||
|
currentRole,
|
||||||
|
isGlobalAdmin,
|
||||||
|
currentCapabilities,
|
||||||
|
accessLoaded,
|
||||||
loading,
|
loading,
|
||||||
|
can,
|
||||||
|
isAtLeast,
|
||||||
fetchWorkspaces,
|
fetchWorkspaces,
|
||||||
switchWorkspace,
|
switchWorkspace,
|
||||||
|
refreshAccess,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user