增加用户权限检查,实现根据用户角色所有pagetype权限控制数据访问
This commit is contained in:
parent
bb379e6c6c
commit
cb39b45b62
@ -15,23 +15,29 @@ import '../assets/styles/main.css'
|
||||
// Font Awesome: 全局引入图标字体样式
|
||||
import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
// 初始化认证状态(需要在pinia初始化之后)
|
||||
import { useAuthStore } from '../shared/stores/auth'
|
||||
const authStore = useAuthStore()
|
||||
authStore.initAuth()
|
||||
|
||||
// 在路由使用前注册工具路由,避免路由匹配时的警告
|
||||
import { useToolsStore } from '../shared/stores/tools'
|
||||
const toolsStore = useToolsStore()
|
||||
toolsStore.initToolRoutes(router)
|
||||
import { usePermissionStore } from '../shared/stores/permissions'
|
||||
import '../shared/utils/fetchInterceptor'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(naive)
|
||||
|
||||
// 初始化fetch拦截器(需要在pinia初始化之后)
|
||||
import '../shared/utils/fetchInterceptor'
|
||||
async function bootstrap() {
|
||||
const authStore = useAuthStore()
|
||||
await authStore.initAuth()
|
||||
|
||||
app.mount('#app')
|
||||
const toolsStore = useToolsStore()
|
||||
toolsStore.initToolRoutes(router)
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
await permissionStore.loadPermissions().catch(() => {})
|
||||
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
|
||||
@ -288,11 +288,13 @@ import { downloadImageToLocal } from '@/shared/api/common'
|
||||
import { usePageTypeSlug } from '@/shared/utils/slug'
|
||||
import { resolvePagetypeToolbarOverride } from '@/core/registry/pagetypeOverride'
|
||||
import DefaultToolbar from '@/core/pagetype/default_toolbar.vue'
|
||||
import { usePermissionStore } from '@/shared/stores/permissions'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
// 使用组合式函数处理URL slug
|
||||
const { pagetypeSlug, entity } = usePageTypeSlug(route)
|
||||
@ -304,6 +306,7 @@ const isNew = computed(() => {
|
||||
return idValue === 'new' || idValue.startsWith('new-')
|
||||
})
|
||||
const canEdit = ref(false)
|
||||
const canRead = computed(() => permissionStore.canRead(entity.value))
|
||||
|
||||
const record = ref<any>({})
|
||||
const originalRecord = ref<any>({})
|
||||
@ -826,9 +829,10 @@ async function loadMeta() {
|
||||
|
||||
metaFields.value = data?.fields || []
|
||||
pageMeta.value = data || {}
|
||||
|
||||
|
||||
canEdit.value = true // 默认支持编辑
|
||||
|
||||
// 权限:默认读取后端权限,决定是否可编辑
|
||||
const pagetypeName = entity.value
|
||||
canEdit.value = permissionStore.canWrite(pagetypeName)
|
||||
|
||||
// 设置默认活动标签:显示第一个标签页
|
||||
if (tabs.value.length > 0) {
|
||||
|
||||
@ -24,7 +24,8 @@
|
||||
createRecordHandler,
|
||||
handleDeleteSelected,
|
||||
router,
|
||||
t
|
||||
t,
|
||||
canEdit: canEditList,
|
||||
}"
|
||||
:entity="entity"
|
||||
:search-query="searchQuery"
|
||||
@ -44,6 +45,7 @@
|
||||
:view-mode="viewMode"
|
||||
:selected-keys="selectedKeys"
|
||||
:loading="loading"
|
||||
:can-edit="canEditList"
|
||||
@update:search-query="searchQuery = $event"
|
||||
@update:view-mode="viewMode = $event"
|
||||
@reload="reload"
|
||||
@ -424,15 +426,18 @@ import {
|
||||
resolvePagetypeListActionsOverride
|
||||
} from '@/core/registry/pagetypeOverride'
|
||||
import { downloadImageToLocal } from '@/shared/api/common'
|
||||
import { usePermissionStore } from '@/shared/stores/permissions'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
// 使用组合式函数处理URL slug
|
||||
const { pagetypeSlug, entity } = usePageTypeSlug(route)
|
||||
const title = computed(() => entity.value)
|
||||
const canEditList = computed(() => permissionStore.canWrite(entity.value))
|
||||
|
||||
// 覆盖组件引用(子组件覆盖,整体覆盖由 ListPage.vue 处理)
|
||||
const toolbarComponent = shallowRef<any | null>(null)
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
<i :class="loading ? 'fa fa-spinner fa-spin' : 'fa fa-refresh'"></i>
|
||||
</button>
|
||||
<button
|
||||
v-if="selectedKeys.length === 0"
|
||||
v-if="selectedKeys.length === 0 && canEditModel"
|
||||
class="create-btn"
|
||||
@click="createRecordHandler"
|
||||
:disabled="loading"
|
||||
@ -39,7 +39,7 @@
|
||||
{{ t('Create') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
v-else-if="canEditModel"
|
||||
class="delete-btn"
|
||||
@click="handleDeleteSelected"
|
||||
:disabled="loading"
|
||||
@ -61,6 +61,7 @@ interface Props {
|
||||
viewMode: 'card' | 'list'
|
||||
selectedKeys: string[]
|
||||
loading: boolean
|
||||
canEdit?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
@ -74,6 +75,8 @@ interface Emits {
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const canEditModel = computed(() => props.canEdit !== false)
|
||||
|
||||
// 使用 computed 来处理双向绑定
|
||||
const searchQueryModel = computed({
|
||||
get: () => props.searchQuery,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { loginApi, getUserInfoApi, logoutApi, isCookieExpired } from '../api/auth'
|
||||
import { usePermissionStore } from './permissions'
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
@ -25,6 +26,10 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const response = await loginApi(username, password)
|
||||
|
||||
if (response.user) {
|
||||
// 清除旧的权限数据
|
||||
const permissionStore = usePermissionStore()
|
||||
permissionStore.clearPermissions()
|
||||
|
||||
user.value = response.user
|
||||
isAuthenticated.value = true
|
||||
|
||||
@ -32,6 +37,9 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
localStorage.setItem('jingrow_user', JSON.stringify(response.user))
|
||||
localStorage.setItem('jingrow_authenticated', 'true')
|
||||
|
||||
// 加载新用户的权限
|
||||
await permissionStore.loadPermissions(true)
|
||||
|
||||
return { success: true, user: response.user }
|
||||
} else {
|
||||
return { success: false, error: response.message || '登录失败' }
|
||||
@ -55,6 +63,10 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
isAuthenticated.value = false
|
||||
localStorage.removeItem('jingrow_user')
|
||||
localStorage.removeItem('jingrow_authenticated')
|
||||
|
||||
// 清除权限数据
|
||||
const permissionStore = usePermissionStore()
|
||||
permissionStore.clearPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useAuthStore } from './auth'
|
||||
import { usePermissionStore } from './permissions'
|
||||
import { slugToPageType } from '../utils/slug'
|
||||
|
||||
export interface AppMenuItem {
|
||||
id: string
|
||||
@ -173,27 +175,23 @@ export const useMenuStore = defineStore('menu', () => {
|
||||
const authStore = useAuthStore()
|
||||
const userType = authStore.user?.user_type
|
||||
|
||||
// 非 System User 用户类型不显示 pagetype 和 workspace 类型的菜单项
|
||||
const isSystemUser = userType === 'System User'
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
return items.value.filter(m => {
|
||||
// 过滤隐藏的菜单项
|
||||
if (m.hidden) return false
|
||||
|
||||
// 非 System User 的过滤逻辑
|
||||
|
||||
// 基本规则:非 System User 的菜单限制(现有逻辑)
|
||||
if (!isSystemUser) {
|
||||
// 过滤掉 pagetype 和 workspace 类型
|
||||
if (m.type === 'pagetype' || m.type === 'workspace') {
|
||||
return false
|
||||
}
|
||||
|
||||
// 只允许显示的根菜单:工具、开发
|
||||
|
||||
const allowedRootMenus = ['tools', 'dev-group']
|
||||
if (!m.parentId && !allowedRootMenus.includes(m.id)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 开发分组下只允许显示:工具市场(非 System User 不显示应用市场、节点市场、智能体市场)
|
||||
|
||||
if (m.parentId === 'dev-group') {
|
||||
const allowedDevMenus = ['tool-marketplace']
|
||||
if (!allowedDevMenus.includes(m.id)) {
|
||||
@ -201,7 +199,16 @@ export const useMenuStore = defineStore('menu', () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 权限驱动规则:pagetype 类型的菜单仅在有 read 权限时显示
|
||||
if (m.type === 'pagetype' && m.pagetype) {
|
||||
const pagetypeName = m.pagetype
|
||||
if (!permissionStore.canRead(pagetypeName)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// workspace 类型暂时不做细粒度 pagetype 检查,由 WorkspacePage 内部再过滤链接
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
100
apps/jingrow/frontend/src/shared/stores/permissions.ts
Normal file
100
apps/jingrow/frontend/src/shared/stores/permissions.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
export interface PagetypePermissions {
|
||||
[permissionType: string]: number
|
||||
}
|
||||
|
||||
export interface UserPermissionsResponse {
|
||||
success: boolean
|
||||
user: string
|
||||
roles: string[]
|
||||
permissions: Record<string, PagetypePermissions>
|
||||
error?: string
|
||||
}
|
||||
|
||||
export const usePermissionStore = defineStore('permissions', () => {
|
||||
const loading = ref(false)
|
||||
const loaded = ref(false)
|
||||
const user = ref<string | null>(null)
|
||||
const roles = ref<string[]>([])
|
||||
const permissions = ref<Record<string, PagetypePermissions>>({})
|
||||
|
||||
async function loadPermissions(force = false) {
|
||||
if (loading.value) return
|
||||
if (loaded.value && !force) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get<UserPermissionsResponse>('/jingrow/user-permissions')
|
||||
const data = response.data
|
||||
|
||||
if (!data || data.success === false) {
|
||||
// 非致命错误:保持为空权限,前端仍然依赖后端校验
|
||||
console.warn('Failed to load user permissions', data?.error)
|
||||
return
|
||||
}
|
||||
|
||||
user.value = data.user
|
||||
roles.value = data.roles || []
|
||||
permissions.value = data.permissions || {}
|
||||
loaded.value = true
|
||||
} catch (error) {
|
||||
console.warn('Error loading user permissions', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function hasPermission(pagetype: string, permissionType: string): boolean {
|
||||
if (!pagetype || !permissionType) return false
|
||||
const perms = permissions.value[pagetype]
|
||||
if (!perms) return false
|
||||
return Boolean(perms[permissionType])
|
||||
}
|
||||
|
||||
function canRead(pagetype: string): boolean {
|
||||
return hasPermission(pagetype, 'read') || hasPermission(pagetype, 'select')
|
||||
}
|
||||
|
||||
function canWrite(pagetype: string): boolean {
|
||||
// 视为“可编辑”:拥有 write 或 create 或 delete 等任一权限
|
||||
return (
|
||||
hasPermission(pagetype, 'write') ||
|
||||
hasPermission(pagetype, 'create') ||
|
||||
hasPermission(pagetype, 'delete') ||
|
||||
hasPermission(pagetype, 'submit') ||
|
||||
hasPermission(pagetype, 'cancel') ||
|
||||
hasPermission(pagetype, 'amend')
|
||||
)
|
||||
}
|
||||
|
||||
// 暴露一个便捷的只读权限 map,用于调试或全局判断
|
||||
const readablePagetypes = computed(() =>
|
||||
Object.keys(permissions.value).filter((pt) => canRead(pt))
|
||||
)
|
||||
|
||||
// 清除权限数据(用户登出时调用)
|
||||
function clearPermissions() {
|
||||
loading.value = false
|
||||
loaded.value = false
|
||||
user.value = null
|
||||
roles.value = []
|
||||
permissions.value = {}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
loaded,
|
||||
user,
|
||||
roles,
|
||||
permissions,
|
||||
loadPermissions,
|
||||
hasPermission,
|
||||
canRead,
|
||||
canWrite,
|
||||
readablePagetypes,
|
||||
clearPermissions,
|
||||
}
|
||||
})
|
||||
@ -49,10 +49,12 @@ import { Icon } from '@iconify/vue'
|
||||
import { t } from '@/shared/i18n'
|
||||
import { getWorkspace } from '@/shared/api/common'
|
||||
import { pageTypeToSlug } from '@/shared/utils/slug'
|
||||
import { usePermissionStore } from '@/shared/stores/permissions'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const name = computed(() => String(route.params.name || 'Build'))
|
||||
const decodedName = computed(() => decodeURIComponent(name.value))
|
||||
@ -95,8 +97,11 @@ async function load() {
|
||||
for (const block of layout || []) {
|
||||
if (block.type === 'card') {
|
||||
const title = block.data?.card_name || 'Section'
|
||||
const col = Number(block.data?.col || 6) // 按 12 栅格,6=两列,3=四列
|
||||
resultItems.push({ key: `card-${title}-${col}-${resultItems.length}` , type:'card', title, col, links: grouped[title] || [], classes: mapColToClasses(col) })
|
||||
const col = Number(block.data?.col || 6)
|
||||
const cardLinks = grouped[title] || []
|
||||
// 如果卡片下所有链接都被权限过滤掉,则跳过该卡片
|
||||
if (!cardLinks.length) continue
|
||||
resultItems.push({ key: `card-${title}-${col}-${resultItems.length}` , type:'card', title, col, links: cardLinks, classes: mapColToClasses(col) })
|
||||
} else if (block.type === 'shortcut') {
|
||||
// 处理快捷方式
|
||||
const title = block.data?.shortcut_name || 'Shortcut'
|
||||
|
||||
@ -6,6 +6,7 @@ import logging
|
||||
|
||||
from jingrow.utils.auth import login, logout, get_user_info, set_context, get_jingrow_cloud_url, get_jingrow_cloud_api_headers
|
||||
from jingrow.config import Config
|
||||
from jingrow.utils.jingrow_api import get_user_pagetype_permissions_from_jingrow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@ -41,6 +42,14 @@ class UserInfoResponse(BaseModel):
|
||||
user_info: Optional[dict] = None
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
class UserPermissionsResponse(BaseModel):
|
||||
success: bool
|
||||
user: Optional[str] = None
|
||||
roles: Optional[list[str]] = None
|
||||
permissions: Optional[dict] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
COOKIE_CONFIG = {
|
||||
"httponly": True,
|
||||
"samesite": "lax",
|
||||
@ -213,3 +222,22 @@ async def get_user_info_route(session_cookie: Optional[str] = Depends(get_sessio
|
||||
except Exception as e:
|
||||
logger.error(f"获取用户信息异常: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"获取用户信息异常: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/jingrow/user-permissions", response_model=UserPermissionsResponse)
|
||||
async def get_user_permissions_route(session_cookie: Optional[str] = Depends(get_session_cookie)):
|
||||
"""获取当前登录用户的 PageType 权限"""
|
||||
if not session_cookie:
|
||||
raise HTTPException(status_code=401, detail="未提供认证信息")
|
||||
|
||||
try:
|
||||
result = get_user_pagetype_permissions_from_jingrow(session_cookie)
|
||||
if not result or not result.get("success"):
|
||||
error_msg = result.get("error") if isinstance(result, dict) else "获取权限失败"
|
||||
raise HTTPException(status_code=400, detail=error_msg)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取用户权限异常: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"获取用户权限异常: {str(e)}")
|
||||
|
||||
@ -225,6 +225,34 @@ def get_ai_settings_from_jingrow():
|
||||
log_error(f"获取AI Settings配置异常: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def get_user_pagetype_permissions_from_jingrow(session_cookie: str | None):
|
||||
"""从 Jingrow 获取当前登录用户在所有 PageType 上的权限信息"""
|
||||
if not session_cookie:
|
||||
return {"success": False, "error": "Session cookie 缺失"}
|
||||
|
||||
api_url = f"{Config.jingrow_server_url}/api/action/jingrow.ai.utils.jlocal.get_user_pagetype_permissions"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Cookie": f"sid={session_cookie}",
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.post(api_url, headers=headers, json={}, timeout=10)
|
||||
if resp.status_code != 200:
|
||||
log_error(f"获取用户权限失败: HTTP {resp.status_code} {resp.text}")
|
||||
return {"success": False, "error": f"HTTP {resp.status_code}"}
|
||||
|
||||
result = resp.json()
|
||||
if isinstance(result, dict) and "message" in result:
|
||||
result = result["message"]
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
log_error(f"获取用户权限异常: {str(e)}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def get_agent_detail(name: str, session_cookie: str = None):
|
||||
"""
|
||||
获取智能体详情
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user