diff --git a/backend/system/management/commands/gen_menu_json.py b/backend/system/management/commands/gen_menu_json.py index 42e14f8..00c7e9e 100644 --- a/backend/system/management/commands/gen_menu_json.py +++ b/backend/system/management/commands/gen_menu_json.py @@ -47,10 +47,12 @@ def gen_menu(app_name, model_name, parent_menu_name, creator='admin'): # 按钮权限 buttons = [ - {"name": "Query", "title": "common.query", "auth_code": f"{app_name}:{model_lower}:query"}, {"name": "Create", "title": "common.create", "auth_code": f"{app_name}:{model_lower}:create"}, {"name": "Edit", "title": "common.edit", "auth_code": f"{app_name}:{model_lower}:edit"}, {"name": "Delete", "title": "common.delete", "auth_code": f"{app_name}:{model_lower}:delete"}, + {"name": "Query", "title": "common.query", "auth_code": f"{app_name}:{model_lower}:query"}, + {"name": "Query", "title": "common.query", "auth_code": f"{app_name}:{model_lower}:import"}, + {"name": "Query", "title": "common.query", "auth_code": f"{app_name}:{model_lower}:export"}, ] for idx, btn in enumerate(buttons): btn_meta = MenuMeta.objects.create( diff --git a/backend/system/views/login_log.py b/backend/system/views/login_log.py index 0c72f6a..e76ecde 100644 --- a/backend/system/views/login_log.py +++ b/backend/system/views/login_log.py @@ -2,6 +2,7 @@ from system.models import LoginLog from utils.serializers import CustomModelSerializer from utils.custom_model_viewSet import CustomModelViewSet from rest_framework import serializers +from utils.permissions import HasButtonPermission class LoginLogSerializer(CustomModelSerializer): """ @@ -28,3 +29,4 @@ class LoginLogViewSet(CustomModelViewSet): search_fields = ['name'] # 根据实际字段调整 ordering_fields = ['create_time', 'id'] ordering = ['-create_time'] + permission_classes = [HasButtonPermission] diff --git a/backend/system/views/user.py b/backend/system/views/user.py index 60ecfd3..77998b3 100644 --- a/backend/system/views/user.py +++ b/backend/system/views/user.py @@ -8,7 +8,6 @@ from django.contrib.auth.hashers import make_password from rest_framework.permissions import IsAuthenticated from system.models import User, Menu, LoginLog -from system.views.menu import MenuSerializer from utils.serializers import CustomModelSerializer from utils.custom_model_viewSet import CustomModelViewSet @@ -79,13 +78,14 @@ class UserInfo(APIView): if user.is_superuser: roles = ['admin'] # menus = Menu.objects.filter(pid__isnull=True).order_by('sort') - # permissions = Menu.objects.filter(type='button').order_by('sort').values_list('auth_code', flat=True) + permissions = Menu.objects.filter(type='button').order_by('auth_code').values_list('auth_code', flat=True) else: roles = user.get_role_name # menus = Menu.objects.filter(pid__isnull=True, role__users=user).order_by('sort').distinct() - # permissions = Menu.objects.filter(type='button', role__users=user).order_by('sort').distinct().values_list('auth_code', flat=True) + permissions = Menu.objects.filter(type='button', role__users=user).order_by('auth_code').distinct().values_list('auth_code', flat=True) # menus_data = MenuSerializer(menus, many=True).data user_data['roles'] = roles + user_data['permissions'] = permissions return Response({ "code": 0, "data": user_data, diff --git a/backend/utils/custom_model_viewSet.py b/backend/utils/custom_model_viewSet.py index 9220273..6a72872 100644 --- a/backend/utils/custom_model_viewSet.py +++ b/backend/utils/custom_model_viewSet.py @@ -20,6 +20,38 @@ class CustomModelViewSet(viewsets.ModelViewSet): # 是否支持软删除 enable_soft_delete = False + def get_required_permission(self): + # 约定:system:menu:create + app_label = self.queryset.model._meta.app_label + model_name = self.queryset.model._meta.model_name + action = self.action # 'create', 'update', 'destroy', 'list', 'retrieve' + # 只对增删改查等操作做权限控制 + action_map = { + 'create': 'create', + 'update': 'edit', + 'partial_update': 'edit', + 'destroy': 'delete', + 'list': 'query', + 'retrieve': 'query', + } + if action in action_map: + perm_action = action_map[action] + else: + perm_action = action # 如 sync、import、export + return f"{app_label}:{model_name}:{perm_action}" + + def get_permissions(self): + permissions = super().get_permissions() + required_code = self.get_required_permission() + if required_code: + from utils.permissions import HasButtonPermission + perm = HasButtonPermission() + # 动态设置 required_permission + perm.required_permission = required_code + permissions.append(perm) + return permissions + + def get_serializer_class(self): """根据当前动作获取序列化器类""" return self.action_serializers.get( @@ -27,13 +59,6 @@ class CustomModelViewSet(viewsets.ModelViewSet): super().get_serializer_class() ) - def get_permissions(self): - """根据当前动作获取权限类""" - permissions = self.action_permissions.get( - self.action, - self.permission_classes - ) - return [permission() for permission in permissions] def list(self, request, *args, **kwargs): """重写列表视图,支持软删除过滤""" diff --git a/backend/utils/permissions.py b/backend/utils/permissions.py index 93fe57e..8f6ab38 100644 --- a/backend/utils/permissions.py +++ b/backend/utils/permissions.py @@ -1,8 +1,48 @@ from rest_framework import permissions +from rest_framework.permissions import BasePermission +from system.models import Menu -class IsSuperUserOrReadOnly(permissions.BasePermission): +class IsSuperUserOrReadOnly(BasePermission): """超级用户可读写,普通用户只读""" def has_permission(self, request, view): if request.method in permissions.SAFE_METHODS: return True return request.user and request.user.is_superuser + + + +class HasButtonPermission(BasePermission): + """ + 通用按钮权限校验 + 用法:在视图中设置 required_permission = 'xxx:xxx:xxx' + """ + def has_permission(self, request, view): + required_code = getattr(view, 'required_permission', None) + if not required_code: + # 可自动推断权限编码逻辑 + app_label = view.queryset.model._meta.app_label + model_name = view.queryset.model._meta.model_name + action = getattr(view, 'action', None) + action_map = { + 'create': 'create', + 'update': 'edit', + 'partial_update': 'edit', + 'destroy': 'delete', + 'list': 'query', + 'retrieve': 'query', + } + if action in action_map: + required_code = f"{app_label}:{model_name}:{action_map[action]}" + if not required_code: + return True # 不需要按钮权限 + user = request.user + if not user.is_authenticated or user.is_anonymous: + return False + if user.is_superuser: + return True + role_ids = user.role.values_list('id', flat=True) + return Menu.objects.filter( + type='button', + role__id__in=role_ids, + auth_code=required_code + ).exists() diff --git a/web/apps/web-antd/src/bootstrap.ts b/web/apps/web-antd/src/bootstrap.ts index ec72112..936edd3 100644 --- a/web/apps/web-antd/src/bootstrap.ts +++ b/web/apps/web-antd/src/bootstrap.ts @@ -15,6 +15,7 @@ import { initComponentAdapter } from './adapter/component'; import { initSetupVbenForm } from './adapter/form'; import App from './app.vue'; import { router } from './router'; +import { registerPermissionDirective } from './utils/permission'; async function bootstrap(namespace: string) { // 初始化组件适配器 @@ -48,6 +49,8 @@ async function bootstrap(namespace: string) { // 安装权限指令 registerAccessDirective(app); + // 注册自定义v-permission指令 + registerPermissionDirective(app); // 初始化 tippy const { initTippy } = await import('@vben/common-ui/es/tippy'); diff --git a/web/apps/web-antd/src/store/auth.ts b/web/apps/web-antd/src/store/auth.ts index bd496d1..2aaa918 100644 --- a/web/apps/web-antd/src/store/auth.ts +++ b/web/apps/web-antd/src/store/auth.ts @@ -13,9 +13,12 @@ import { defineStore } from 'pinia'; import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api'; import { $t } from '#/locales'; +import { usePermissionStore } from './permission'; + export const useAuthStore = defineStore('auth', () => { const accessStore = useAccessStore(); const userStore = useUserStore(); + const permissionStore = usePermissionStore(); const router = useRouter(); const loginLoading = ref(false); @@ -62,7 +65,7 @@ export const useAuthStore = defineStore('auth', () => { if (userInfo?.realName) { notification.success({ - description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, + description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.username}`, duration: 3, message: $t('authentication.loginSuccess'), }); @@ -101,6 +104,11 @@ export const useAuthStore = defineStore('auth', () => { let userInfo: null | UserInfo = null; userInfo = await getUserInfoApi(); userStore.setUserInfo(userInfo); + // 设置权限 + if (userInfo && Array.isArray(userInfo.permissions)) { + permissionStore.setPermissions(userInfo.permissions); + } + return userInfo; } diff --git a/web/apps/web-antd/src/store/permission.ts b/web/apps/web-antd/src/store/permission.ts new file mode 100644 index 0000000..f46d6d8 --- /dev/null +++ b/web/apps/web-antd/src/store/permission.ts @@ -0,0 +1,30 @@ +import { ref } from 'vue'; + +import { defineStore } from 'pinia'; + +export const usePermissionStore = defineStore('permission', () => { + // 权限码列表 + const permissions = ref([]); + + // 设置权限码 + function setPermissions(perms: string[]) { + permissions.value = perms || []; + } + + // 判断是否有某个权限 + function hasPermission(code: string): boolean { + return permissions.value.includes(code); + } + + // 重置 + function $reset() { + permissions.value = []; + } + + return { + permissions, + setPermissions, + hasPermission, + $reset, + }; +}) diff --git a/web/apps/web-antd/src/utils/permission.ts b/web/apps/web-antd/src/utils/permission.ts new file mode 100644 index 0000000..be93c50 --- /dev/null +++ b/web/apps/web-antd/src/utils/permission.ts @@ -0,0 +1,48 @@ +import type { App, DirectiveBinding } from 'vue'; + +import { usePermissionStore } from '#/store/permission'; + +/** + * 权限按钮option生成工具 + * @param code 权限码 + * @param option 按钮option或字符串 + * @returns 有权限返回option,无权限返回false + */ +export function op(code: string, option: any) { + const permissionStore = usePermissionStore(); + return permissionStore.hasPermission(code) ? option : false; +} + +/** + * 全局权限判断函数,适用于模板v-if + */ +export function hasPermission(code: string): boolean { + const permissionStore = usePermissionStore(); + return permissionStore.hasPermission(code); +} + +/** + * v-permission 自定义指令 + * 用法:v-permission="'system:user:create'" + * 或 v-permission="['system:user:create', 'system:user:update']" + */ +const permissionDirective = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + const codes = Array.isArray(binding.value) + ? binding.value + : [binding.value]; + const permissionStore = usePermissionStore(); + const has = codes.some((code) => permissionStore.hasPermission(code)); + if (!has) { + el.parentNode && el.remove(); + } + }, +}; + +/** + * 注册全局权限指令 + * @param app Vue App 实例 + */ +export function registerPermissionDirective(app: App) { + app.directive('permission', permissionDirective); +} diff --git a/web/apps/web-antd/src/views/system/dept/data.ts b/web/apps/web-antd/src/views/system/dept/data.ts index 135685a..8255e4c 100644 --- a/web/apps/web-antd/src/views/system/dept/data.ts +++ b/web/apps/web-antd/src/views/system/dept/data.ts @@ -8,6 +8,8 @@ import { z } from '#/adapter/form'; import { getDeptList } from '#/api/system/dept'; import { $t } from '#/locales'; import { format_datetime } from '#/utils/date'; +import { op } from '#/utils/permission'; + /** * 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量 */ @@ -137,18 +139,15 @@ export function useColumns( }, name: 'CellOperation', options: [ - { - code: 'append', - text: '新增下级', - }, - 'edit', // 默认的编辑按钮 - { - code: 'delete', // 默认的删除按钮 + op('system:dept:create', { code: 'append', text: '新增下级' }), + op('system:dept:edit', 'edit'), + op('system:dept:delete', { + code: 'delete', disabled: (row: SystemDeptApi.SystemDept) => { return !!(row.children && row.children.length > 0); }, - }, - ], + }), + ].filter(Boolean), }, field: 'operation', fixed: 'right', diff --git a/web/apps/web-antd/src/views/system/dept/list.vue b/web/apps/web-antd/src/views/system/dept/list.vue index 1b4154d..2f64006 100644 --- a/web/apps/web-antd/src/views/system/dept/list.vue +++ b/web/apps/web-antd/src/views/system/dept/list.vue @@ -133,7 +133,11 @@ function refreshGrid() {