增加用户权限检查,实现根据用户角色所有pagetype权限控制数据访问

This commit is contained in:
jingrow 2026-01-25 03:20:53 +08:00
parent bb379e6c6c
commit cb39b45b62
10 changed files with 230 additions and 32 deletions

View File

@ -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()

View File

@ -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) {

View File

@ -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)

View File

@ -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,

View File

@ -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()
}
}

View File

@ -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
})
})

View 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,
}
})

View File

@ -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'

View File

@ -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)}")

View File

@ -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):
"""
获取智能体详情