diff --git a/README.md b/README.md index 019582d7..a9548989 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ See a preview of some of the screenshots - [x] Global SQLAlchemy 2.0 syntax - [x] Pydantic v1 and v2 (different branches) - [x] Casbin RBAC access control model +- [x] Role menu RBAC access control model - [x] Celery asynchronous tasks - [x] JWT middleware whitelist authentication - [x] Global customizable time zone time @@ -218,18 +219,13 @@ Execute unittests via pytest - [Ruff](https://beta.ruff.rs/docs/) - ... -## 互动 +## Interactivity We only have one current channel. - - - - - - - -
Jump
Telegram
+| [Jump](https://t.me/+ZlPhIFkPp7E4NGI1) | +|----------------------------------------| +| Telegram | ## Sponsor us diff --git a/README.zh-CN.md b/README.zh-CN.md index 60310a00..2a102a89 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -47,6 +47,7 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三 - [x] 全局 SQLAlchemy 2.0 语法 - [x] Pydantic v1 和 v2 (不同分支) - [x] Casbin RBAC 访问控制模型 +- [x] 角色菜单 RBAC 访问控制模型 - [x] Celery 异步任务 - [x] JWT 中间件白名单认证 - [x] 全局自定义时区时间 @@ -216,14 +217,9 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三 有且仅有当前一个频道,请注意辨别真伪 - - - - - - - -
直链跳转
Telegram(科学上网)
+| [直链跳转](https://t.me/+ZlPhIFkPp7E4NGI1) | +|----------------------------------------| +| Telegram(科学上网) | ## 赞助我们 diff --git a/backend/app/api/v1/api.py b/backend/app/api/v1/api.py index bbe88193..4b1a1e27 100644 --- a/backend/app/api/v1/api.py +++ b/backend/app/api/v1/api.py @@ -2,10 +2,11 @@ # -*- coding: utf-8 -*- from typing import Annotated -from fastapi import APIRouter, Query +from fastapi import APIRouter, Depends, Query from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import PageDepends, paging_data +from backend.app.common.pagination import DependsPagination, paging_data +from backend.app.common.permission import RequestPermission from backend.app.common.rbac import DependsRBAC from backend.app.common.response.response_schema import response_base from backend.app.database.db_mysql import CurrentSession @@ -27,7 +28,14 @@ async def get_api(pk: int): return await response_base.success(data=api) -@router.get('', summary='(模糊条件)分页获取所有接口', dependencies=[DependsJwtAuth, PageDepends]) +@router.get( + '', + summary='(模糊条件)分页获取所有接口', + dependencies=[ + DependsJwtAuth, + DependsPagination, + ], +) async def get_api_list( db: CurrentSession, name: Annotated[str | None, Query()] = None, @@ -39,13 +47,27 @@ async def get_api_list( return await response_base.success(data=page_data) -@router.post('', summary='创建接口', dependencies=[DependsRBAC]) +@router.post( + '', + summary='创建接口', + dependencies=[ + Depends(RequestPermission('sys:api:add')), + DependsRBAC, + ], +) async def create_api(obj: CreateApi): await ApiService.create(obj=obj) return await response_base.success() -@router.put('/{pk}', summary='更新接口', dependencies=[DependsRBAC]) +@router.put( + '/{pk}', + summary='更新接口', + dependencies=[ + Depends(RequestPermission('sys:api:edit')), + DependsRBAC, + ], +) async def update_api(pk: int, obj: UpdateApi): count = await ApiService.update(pk=pk, obj=obj) if count > 0: @@ -53,7 +75,14 @@ async def update_api(pk: int, obj: UpdateApi): return await response_base.fail() -@router.delete('', summary='(批量)删除接口', dependencies=[DependsRBAC]) +@router.delete( + '', + summary='(批量)删除接口', + dependencies=[ + Depends(RequestPermission('sys:api:del')), + DependsRBAC, + ], +) async def delete_api(pk: Annotated[list[int], Query(...)]): count = await ApiService.delete(pk=pk) if count > 0: diff --git a/backend/app/api/v1/auth/auth.py b/backend/app/api/v1/auth/auth.py index 050b688a..f7d53e23 100644 --- a/backend/app/api/v1/auth/auth.py +++ b/backend/app/api/v1/auth/auth.py @@ -16,7 +16,12 @@ from backend.app.services.auth_service import AuthService router = APIRouter() -@router.post('/swagger_login', summary='swagger 表单登录', description='form 格式登录,仅用于 swagger 文档调试接口') +@router.post( + '/swagger_login', + summary='swagger 表单登录', + description='form 格式登录,用于 swagger 文档调试以及获取 JWT Auth', + deprecated=True, +) async def swagger_user_login(form_data: OAuth2PasswordRequestForm = Depends()) -> GetSwaggerToken: token, user = await AuthService().swagger_login(form_data=form_data) return GetSwaggerToken(access_token=token, user=user) # type: ignore @@ -25,7 +30,7 @@ async def swagger_user_login(form_data: OAuth2PasswordRequestForm = Depends()) - @router.post( '/login', summary='用户登录', - description='json 格式登录, 仅支持在第三方api工具调试接口, 例如: postman', + description='json 格式登录, 仅支持在第三方api工具调试, 例如: postman', dependencies=[Depends(RateLimiter(times=5, minutes=1))], ) async def user_login(request: Request, obj: AuthLogin, background_tasks: BackgroundTasks): diff --git a/backend/app/api/v1/casbin.py b/backend/app/api/v1/casbin.py index 34cbb801..75672b0a 100644 --- a/backend/app/api/v1/casbin.py +++ b/backend/app/api/v1/casbin.py @@ -2,10 +2,11 @@ # -*- coding: utf-8 -*- from typing import Annotated -from fastapi import APIRouter, Path, Query +from fastapi import APIRouter, Depends, Path, Query from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import PageDepends, paging_data +from backend.app.common.pagination import DependsPagination, paging_data +from backend.app.common.permission import RequestPermission from backend.app.common.rbac import DependsRBAC from backend.app.common.response.response_schema import response_base from backend.app.database.db_mysql import CurrentSession @@ -13,7 +14,6 @@ from backend.app.schemas.casbin_rule import ( CreatePolicy, CreateUserRole, DeleteAllPolicies, - DeleteAllUserRoles, DeletePolicy, DeleteUserRole, GetAllPolicy, @@ -24,11 +24,18 @@ from backend.app.services.casbin_service import CasbinService router = APIRouter() -@router.get('', summary='(模糊条件)分页获取所有权限规则', dependencies=[DependsJwtAuth, PageDepends]) +@router.get( + '', + summary='(模糊条件)分页获取所有权限规则', + dependencies=[ + DependsJwtAuth, + DependsPagination, + ], +) async def get_all_casbin( db: CurrentSession, - ptype: Annotated[str | None, Query()] = None, - sub: Annotated[str | None, Query()] = None, + ptype: Annotated[str | None, Query(description='规则类型, p / g')] = None, + sub: Annotated[str | None, Query(description='用户 uuid / 角色')] = None, ): casbin_select = await CasbinService.get_casbin_list(ptype=ptype, sub=sub) page_data = await paging_data(db, casbin_select, GetAllPolicy) @@ -47,7 +54,14 @@ async def get_role_policies(role: Annotated[str, Path(description='角色ID')]): return await response_base.success(data=policies) -@router.post('/policy', summary='添加P权限规则', dependencies=[DependsRBAC]) +@router.post( + '/policy', + summary='添加P权限规则', + dependencies=[ + Depends(RequestPermission('casbin:p:add')), + DependsRBAC, + ], +) async def create_policy(p: CreatePolicy): """ p 规则: @@ -62,37 +76,79 @@ async def create_policy(p: CreatePolicy): return await response_base.success(data=data) -@router.post('/policies', summary='添加多组P权限规则', dependencies=[DependsRBAC]) +@router.post( + '/policies', + summary='添加多组P权限规则', + dependencies=[ + Depends(RequestPermission('casbin:p:group:add')), + DependsRBAC, + ], +) async def create_policies(ps: list[CreatePolicy]): data = await CasbinService.create_policies(ps=ps) return await response_base.success(data=data) -@router.put('/policy', summary='更新P权限规则', dependencies=[DependsRBAC]) +@router.put( + '/policy', + summary='更新P权限规则', + dependencies=[ + Depends(RequestPermission('casbin:p:edit')), + DependsRBAC, + ], +) async def update_policy(old: UpdatePolicy, new: UpdatePolicy): data = await CasbinService.update_policy(old=old, new=new) return await response_base.success(data=data) -@router.put('/policies', summary='更新多组P权限规则', dependencies=[DependsRBAC]) +@router.put( + '/policies', + summary='更新多组P权限规则', + dependencies=[ + Depends(RequestPermission('casbin:p:group:edit')), + DependsRBAC, + ], +) async def update_policies(old: list[UpdatePolicy], new: list[UpdatePolicy]): data = await CasbinService.update_policies(old=old, new=new) return await response_base.success(data=data) -@router.delete('/policy', summary='删除P权限规则', dependencies=[DependsRBAC]) +@router.delete( + '/policy', + summary='删除P权限规则', + dependencies=[ + Depends(RequestPermission('casbin:p:del')), + DependsRBAC, + ], +) async def delete_policy(p: DeletePolicy): data = await CasbinService.delete_policy(p=p) return await response_base.success(data=data) -@router.delete('/policies', summary='删除多组P权限规则', dependencies=[DependsRBAC]) +@router.delete( + '/policies', + summary='删除多组P权限规则', + dependencies=[ + Depends(RequestPermission('casbin:p:group:del')), + DependsRBAC, + ], +) async def delete_policies(ps: list[DeletePolicy]): data = await CasbinService.delete_policies(ps=ps) return await response_base.success(data=data) -@router.delete('/policies/all', summary='删除所有P权限规则', dependencies=[DependsRBAC]) +@router.delete( + '/policies/all', + summary='删除所有P权限规则', + dependencies=[ + Depends(RequestPermission('casbin:p:empty')), + DependsRBAC, + ], +) async def delete_all_policies(sub: DeleteAllPolicies): count = await CasbinService.delete_all_policies(sub=sub) if count > 0: @@ -106,7 +162,14 @@ async def get_all_groups(): return await response_base.success(data=data) -@router.post('/group', summary='添加G权限规则', dependencies=[DependsRBAC]) +@router.post( + '/group', + summary='添加G权限规则', + dependencies=[ + Depends(RequestPermission('casbin:g:add')), + DependsRBAC, + ], +) async def create_group(g: CreateUserRole): """ g 规则 (**依赖 p 规则**): @@ -121,26 +184,54 @@ async def create_group(g: CreateUserRole): return await response_base.success(data=data) -@router.post('/groups', summary='添加多组G权限规则', dependencies=[DependsRBAC]) +@router.post( + '/groups', + summary='添加多组G权限规则', + dependencies=[ + Depends(RequestPermission('casbin:g:group:add')), + DependsRBAC, + ], +) async def create_groups(gs: list[CreateUserRole]): data = await CasbinService.create_groups(gs=gs) return await response_base.success(data=data) -@router.delete('/group', summary='删除G权限规则', dependencies=[DependsRBAC]) +@router.delete( + '/group', + summary='删除G权限规则', + dependencies=[ + Depends(RequestPermission('casbin:g:del')), + DependsRBAC, + ], +) async def delete_group(g: DeleteUserRole): data = await CasbinService.delete_group(g=g) return await response_base.success(data=data) -@router.delete('/groups', summary='删除多组G权限规则', dependencies=[DependsRBAC]) +@router.delete( + '/groups', + summary='删除多组G权限规则', + dependencies=[ + Depends(RequestPermission('casbin:g:group:del')), + DependsRBAC, + ], +) async def delete_groups(gs: list[DeleteUserRole]): data = await CasbinService.delete_groups(gs=gs) return await response_base.success(data=data) -@router.delete('/groups/all', summary='删除所有G权限规则', dependencies=[DependsRBAC]) -async def delete_all_groups(uuid: DeleteAllUserRoles): +@router.delete( + '/groups/all', + summary='删除所有G权限规则', + dependencies=[ + Depends(RequestPermission('casbin:g:empty')), + DependsRBAC, + ], +) +async def delete_all_groups(uuid: str): count = await CasbinService.delete_all_groups(uuid=uuid) if count > 0: return await response_base.success() diff --git a/backend/app/api/v1/dept.py b/backend/app/api/v1/dept.py index 94cab232..f3ba41e8 100644 --- a/backend/app/api/v1/dept.py +++ b/backend/app/api/v1/dept.py @@ -2,9 +2,10 @@ # -*- coding: utf-8 -*- from typing import Annotated -from fastapi import APIRouter, Query +from fastapi import APIRouter, Depends, Query from backend.app.common.jwt import DependsJwtAuth +from backend.app.common.permission import RequestPermission from backend.app.common.rbac import DependsRBAC from backend.app.common.response.response_schema import response_base from backend.app.schemas.dept import CreateDept, GetAllDept, UpdateDept @@ -32,13 +33,27 @@ async def get_all_depts( return await response_base.success(data=dept) -@router.post('', summary='创建部门', dependencies=[DependsRBAC]) +@router.post( + '', + summary='创建部门', + dependencies=[ + Depends(RequestPermission('sys:dept:add')), + DependsRBAC, + ], +) async def create_dept(obj: CreateDept): await DeptService.create(obj=obj) return await response_base.success() -@router.put('/{pk}', summary='更新部门', dependencies=[DependsRBAC]) +@router.put( + '/{pk}', + summary='更新部门', + dependencies=[ + Depends(RequestPermission('sys:dept:edit')), + DependsRBAC, + ], +) async def update_dept(pk: int, obj: UpdateDept): count = await DeptService.update(pk=pk, obj=obj) if count > 0: @@ -46,7 +61,14 @@ async def update_dept(pk: int, obj: UpdateDept): return await response_base.fail() -@router.delete('{pk}', summary='删除部门', dependencies=[DependsRBAC]) +@router.delete( + '{pk}', + summary='删除部门', + dependencies=[ + Depends(RequestPermission('sys:dept:del')), + DependsRBAC, + ], +) async def delete_dept(pk: int): count = await DeptService.delete(pk=pk) if count > 0: diff --git a/backend/app/api/v1/dict_data.py b/backend/app/api/v1/dict_data.py index 6337c9d0..af27b2a9 100644 --- a/backend/app/api/v1/dict_data.py +++ b/backend/app/api/v1/dict_data.py @@ -2,10 +2,11 @@ # -*- coding: utf-8 -*- from typing import Annotated -from fastapi import APIRouter, Query +from fastapi import APIRouter, Depends, Query from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import PageDepends, paging_data +from backend.app.common.pagination import DependsPagination, paging_data +from backend.app.common.permission import RequestPermission from backend.app.common.rbac import DependsRBAC from backend.app.common.response.response_schema import response_base from backend.app.database.db_mysql import CurrentSession @@ -23,7 +24,14 @@ async def get_dict_data(pk: int): return await response_base.success(data=data) -@router.get('', summary='(模糊条件)分页获取所有字典', dependencies=[DependsJwtAuth, PageDepends]) +@router.get( + '', + summary='(模糊条件)分页获取所有字典', + dependencies=[ + DependsJwtAuth, + DependsPagination, + ], +) async def get_all_dict_datas( db: CurrentSession, label: Annotated[str | None, Query()] = None, @@ -35,13 +43,27 @@ async def get_all_dict_datas( return await response_base.success(data=page_data) -@router.post('', summary='创建字典', dependencies=[DependsRBAC]) +@router.post( + '', + summary='创建字典', + dependencies=[ + Depends(RequestPermission('sys:dict:data:add')), + DependsRBAC, + ], +) async def create_dict_data(obj: CreateDictData): await DictDataService.create(obj=obj) return await response_base.success() -@router.put('/{pk}', summary='更新字典', dependencies=[DependsRBAC]) +@router.put( + '/{pk}', + summary='更新字典', + dependencies=[ + Depends(RequestPermission('sys:dict:data:edit')), + DependsRBAC, + ], +) async def update_dict_data(pk: int, obj: UpdateDictData): count = await DictDataService.update(pk=pk, obj=obj) if count > 0: @@ -49,7 +71,14 @@ async def update_dict_data(pk: int, obj: UpdateDictData): return await response_base.fail() -@router.delete('', summary='(批量)删除字典', dependencies=[DependsRBAC]) +@router.delete( + '', + summary='(批量)删除字典', + dependencies=[ + Depends(RequestPermission('sys:dict:data:del')), + DependsRBAC, + ], +) async def delete_dict_data(pk: Annotated[list[int], Query(...)]): count = await DictDataService.delete(pk=pk) if count > 0: diff --git a/backend/app/api/v1/dict_type.py b/backend/app/api/v1/dict_type.py index 4d141977..e3469949 100644 --- a/backend/app/api/v1/dict_type.py +++ b/backend/app/api/v1/dict_type.py @@ -2,10 +2,11 @@ # -*- coding: utf-8 -*- from typing import Annotated -from fastapi import APIRouter, Query +from fastapi import APIRouter, Depends, Query from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import PageDepends, paging_data +from backend.app.common.pagination import DependsPagination, paging_data +from backend.app.common.permission import RequestPermission from backend.app.common.rbac import DependsRBAC from backend.app.common.response.response_schema import response_base from backend.app.database.db_mysql import CurrentSession @@ -15,7 +16,14 @@ from backend.app.services.dict_type_service import DictTypeService router = APIRouter() -@router.get('', summary='(模糊条件)分页获取所有字典类型', dependencies=[DependsJwtAuth, PageDepends]) +@router.get( + '', + summary='(模糊条件)分页获取所有字典类型', + dependencies=[ + DependsJwtAuth, + DependsPagination, + ], +) async def get_all_dict_types( db: CurrentSession, name: Annotated[str | None, Query()] = None, @@ -27,13 +35,27 @@ async def get_all_dict_types( return await response_base.success(data=page_data) -@router.post('', summary='创建字典类型', dependencies=[DependsRBAC]) +@router.post( + '', + summary='创建字典类型', + dependencies=[ + Depends(RequestPermission('sys:dict:type:add')), + DependsRBAC, + ], +) async def create_dict_type(obj: CreateDictType): await DictTypeService.create(obj=obj) return await response_base.success() -@router.put('/{pk}', summary='更新字典类型', dependencies=[DependsRBAC]) +@router.put( + '/{pk}', + summary='更新字典类型', + dependencies=[ + Depends(RequestPermission('sys:dict:type:edit')), + DependsRBAC, + ], +) async def update_dict_type(pk: int, obj: UpdateDictType): count = await DictTypeService.update(pk=pk, obj=obj) if count > 0: @@ -41,7 +63,14 @@ async def update_dict_type(pk: int, obj: UpdateDictType): return await response_base.fail() -@router.delete('', summary='(批量)删除字典类型', dependencies=[DependsRBAC]) +@router.delete( + '', + summary='(批量)删除字典类型', + dependencies=[ + Depends(RequestPermission('sys:dict:type:del')), + DependsRBAC, + ], +) async def delete_dict_type(pk: Annotated[list[int], Query(...)]): count = await DictTypeService.delete(pk=pk) if count > 0: diff --git a/backend/app/api/v1/log/login_log.py b/backend/app/api/v1/log/login_log.py index dac8e77b..70cd3a5c 100644 --- a/backend/app/api/v1/log/login_log.py +++ b/backend/app/api/v1/log/login_log.py @@ -2,10 +2,11 @@ # -*- coding: utf-8 -*- from typing import Annotated -from fastapi import APIRouter, Query +from fastapi import APIRouter, Depends, Query from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import PageDepends, paging_data +from backend.app.common.pagination import DependsPagination, paging_data +from backend.app.common.permission import RequestPermission from backend.app.common.rbac import DependsRBAC from backend.app.common.response.response_schema import response_base from backend.app.database.db_mysql import CurrentSession @@ -15,7 +16,14 @@ from backend.app.services.login_log_service import LoginLogService router = APIRouter() -@router.get('', summary='(模糊条件)分页获取登录日志', dependencies=[DependsJwtAuth, PageDepends]) +@router.get( + '', + summary='(模糊条件)分页获取登录日志', + dependencies=[ + DependsJwtAuth, + DependsPagination, + ], +) async def get_all_login_logs( db: CurrentSession, username: Annotated[str | None, Query()] = None, @@ -27,7 +35,14 @@ async def get_all_login_logs( return await response_base.success(data=page_data) -@router.delete('', summary='(批量)删除登录日志', dependencies=[DependsRBAC]) +@router.delete( + '', + summary='(批量)删除登录日志', + dependencies=[ + Depends(RequestPermission('log:login:del')), + DependsRBAC, + ], +) async def delete_login_log(pk: Annotated[list[int], Query(...)]): count = await LoginLogService.delete(pk=pk) if count > 0: @@ -35,7 +50,14 @@ async def delete_login_log(pk: Annotated[list[int], Query(...)]): return await response_base.fail() -@router.delete('/all', summary='清空登录日志', dependencies=[DependsRBAC]) +@router.delete( + '/all', + summary='清空登录日志', + dependencies=[ + Depends(RequestPermission('log:login:empty')), + DependsRBAC, + ], +) async def delete_all_login_logs(): count = await LoginLogService.delete_all() if count > 0: diff --git a/backend/app/api/v1/log/opera_log.py b/backend/app/api/v1/log/opera_log.py index 1170769f..922ec43d 100644 --- a/backend/app/api/v1/log/opera_log.py +++ b/backend/app/api/v1/log/opera_log.py @@ -2,10 +2,11 @@ # -*- coding: utf-8 -*- from typing import Annotated -from fastapi import APIRouter, Query +from fastapi import APIRouter, Depends, Query from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import PageDepends, paging_data +from backend.app.common.pagination import DependsPagination, paging_data +from backend.app.common.permission import RequestPermission from backend.app.common.rbac import DependsRBAC from backend.app.common.response.response_schema import response_base from backend.app.database.db_mysql import CurrentSession @@ -15,7 +16,14 @@ from backend.app.services.opera_log_service import OperaLogService router = APIRouter() -@router.get('', summary='(模糊条件)分页获取操作日志', dependencies=[DependsJwtAuth, PageDepends]) +@router.get( + '', + summary='(模糊条件)分页获取操作日志', + dependencies=[ + DependsJwtAuth, + DependsPagination, + ], +) async def get_all_opera_logs( db: CurrentSession, username: Annotated[str | None, Query()] = None, @@ -27,7 +35,14 @@ async def get_all_opera_logs( return await response_base.success(data=page_data) -@router.delete('', summary='(批量)删除操作日志', dependencies=[DependsRBAC]) +@router.delete( + '', + summary='(批量)删除操作日志', + dependencies=[ + Depends(RequestPermission('log:opera:del')), + DependsRBAC, + ], +) async def delete_opera_log(pk: Annotated[list[int], Query(...)]): count = await OperaLogService.delete(pk=pk) if count > 0: @@ -35,7 +50,14 @@ async def delete_opera_log(pk: Annotated[list[int], Query(...)]): return await response_base.fail() -@router.delete('/all', summary='清空操作日志', dependencies=[DependsRBAC]) +@router.delete( + '/all', + summary='清空操作日志', + dependencies=[ + Depends(RequestPermission('log:opera:empty')), + DependsRBAC, + ], +) async def delete_all_opera_logs(): count = await OperaLogService.delete_all() if count > 0: diff --git a/backend/app/api/v1/menu.py b/backend/app/api/v1/menu.py index f06ffd28..04dd9191 100644 --- a/backend/app/api/v1/menu.py +++ b/backend/app/api/v1/menu.py @@ -2,9 +2,10 @@ # -*- coding: utf-8 -*- from typing import Annotated -from fastapi import APIRouter, Query, Request +from fastapi import APIRouter, Depends, Query, Request from backend.app.common.jwt import DependsJwtAuth +from backend.app.common.permission import RequestPermission from backend.app.common.rbac import DependsRBAC from backend.app.common.response.response_schema import response_base from backend.app.schemas.menu import CreateMenu, GetAllMenu, UpdateMenu @@ -36,13 +37,27 @@ async def get_all_menus( return await response_base.success(data=menu) -@router.post('', summary='创建菜单', dependencies=[DependsRBAC]) +@router.post( + '', + summary='创建菜单', + dependencies=[ + Depends(RequestPermission('sys:menu:add')), + DependsRBAC, + ], +) async def create_menu(obj: CreateMenu): await MenuService.create(obj=obj) return await response_base.success() -@router.put('/{pk}', summary='更新菜单', dependencies=[DependsRBAC]) +@router.put( + '/{pk}', + summary='更新菜单', + dependencies=[ + Depends(RequestPermission('sys:menu:edit')), + DependsRBAC, + ], +) async def update_menu(pk: int, obj: UpdateMenu): count = await MenuService.update(pk=pk, obj=obj) if count > 0: @@ -50,7 +65,14 @@ async def update_menu(pk: int, obj: UpdateMenu): return await response_base.fail() -@router.delete('/{pk}', summary='删除菜单', dependencies=[DependsRBAC]) +@router.delete( + '/{pk}', + summary='删除菜单', + dependencies=[ + Depends(RequestPermission('sys:menu:del')), + DependsRBAC, + ], +) async def delete_menu(pk: int): count = await MenuService.delete(pk=pk) if count > 0: diff --git a/backend/app/api/v1/mixed/config.py b/backend/app/api/v1/mixed/config.py index bdc7dec1..4e117680 100644 --- a/backend/app/api/v1/mixed/config.py +++ b/backend/app/api/v1/mixed/config.py @@ -1,15 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from fastapi import APIRouter, Request +from fastapi import APIRouter, Depends, Request from fastapi.routing import APIRoute +from backend.app.common.permission import RequestPermission from backend.app.common.rbac import DependsRBAC from backend.app.common.response.response_schema import response_base router = APIRouter() -@router.get('/routers', summary='获取所有路由', dependencies=[DependsRBAC]) +@router.get( + '/routers', + summary='获取所有路由', + dependencies=[ + Depends(RequestPermission('sys:route:list')), + DependsRBAC, + ], +) async def get_all_route(request: Request): data = [] for route in request.app.routes: diff --git a/backend/app/api/v1/monitor/redis.py b/backend/app/api/v1/monitor/redis.py index 3719fd91..8f6ff6cc 100644 --- a/backend/app/api/v1/monitor/redis.py +++ b/backend/app/api/v1/monitor/redis.py @@ -1,15 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from fastapi import APIRouter +from fastapi import APIRouter, Depends from backend.app.common.jwt import DependsJwtAuth +from backend.app.common.permission import RequestPermission from backend.app.common.response.response_schema import response_base from backend.app.utils.redis_info import redis_info router = APIRouter() -@router.get('/redis', summary='redis 监控', dependencies=[DependsJwtAuth]) +@router.get( + '/redis', + summary='redis 监控', + dependencies=[ + Depends(RequestPermission('sys:monitor:redis')), + DependsJwtAuth, + ], +) async def get_redis_info(): data = {'info': await redis_info.get_info(), 'stats': await redis_info.get_stats()} return await response_base.success(data=data) diff --git a/backend/app/api/v1/monitor/server.py b/backend/app/api/v1/monitor/server.py index 9e06917b..47aa4599 100644 --- a/backend/app/api/v1/monitor/server.py +++ b/backend/app/api/v1/monitor/server.py @@ -1,16 +1,24 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from fastapi import APIRouter +from fastapi import APIRouter, Depends from starlette.concurrency import run_in_threadpool from backend.app.common.jwt import DependsJwtAuth +from backend.app.common.permission import RequestPermission from backend.app.common.response.response_schema import response_base from backend.app.utils.server_info import server_info router = APIRouter() -@router.get('/server', summary='server 监控', dependencies=[DependsJwtAuth]) +@router.get( + '/server', + summary='server 监控', + dependencies=[ + Depends(RequestPermission('sys:monitor:server')), + DependsJwtAuth, + ], +) async def get_server_info(): """IO密集型任务,使用线程池尽量减少性能损耗""" data = { diff --git a/backend/app/api/v1/role.py b/backend/app/api/v1/role.py index 1e9a44b9..cdb7dc58 100644 --- a/backend/app/api/v1/role.py +++ b/backend/app/api/v1/role.py @@ -2,10 +2,11 @@ # -*- coding: utf-8 -*- from typing import Annotated -from fastapi import APIRouter, Query +from fastapi import APIRouter, Depends, Query, Request from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import PageDepends, paging_data +from backend.app.common.pagination import DependsPagination, paging_data +from backend.app.common.permission import RequestPermission from backend.app.common.rbac import DependsRBAC from backend.app.common.response.response_schema import response_base from backend.app.database.db_mysql import CurrentSession @@ -44,7 +45,14 @@ async def get_role(pk: int): return await response_base.success(data=data) -@router.get('', summary='(模糊条件)分页获取所有角色', dependencies=[DependsJwtAuth, PageDepends]) +@router.get( + '', + summary='(模糊条件)分页获取所有角色', + dependencies=[ + DependsJwtAuth, + DependsPagination, + ], +) async def get_all_role_list( db: CurrentSession, name: Annotated[str | None, Query()] = None, @@ -56,13 +64,27 @@ async def get_all_role_list( return await response_base.success(data=page_data) -@router.post('', summary='创建角色', dependencies=[DependsRBAC]) +@router.post( + '', + summary='创建角色', + dependencies=[ + Depends(RequestPermission('sys:role:add')), + DependsRBAC, + ], +) async def create_role(obj: CreateRole): await RoleService.create(obj=obj) return await response_base.success() -@router.put('/{pk}', summary='更新角色', dependencies=[DependsRBAC]) +@router.put( + '/{pk}', + summary='更新角色', + dependencies=[ + Depends(RequestPermission('sys:role:edit')), + DependsRBAC, + ], +) async def update_role(pk: int, obj: UpdateRole): count = await RoleService.update(pk=pk, obj=obj) if count > 0: @@ -70,15 +92,29 @@ async def update_role(pk: int, obj: UpdateRole): return await response_base.fail() -@router.put('/{pk}/menu', summary='更新角色菜单', dependencies=[DependsRBAC]) -async def update_role_menu(pk: int, menu_ids: UpdateRoleMenu): - count = await RoleService.update_menus(pk=pk, menu_ids=menu_ids) +@router.put( + '/{pk}/menu', + summary='更新角色菜单', + dependencies=[ + Depends(RequestPermission('sys:role:menu:edit')), + DependsRBAC, + ], +) +async def update_role_menu(request: Request, pk: int, menu_ids: UpdateRoleMenu): + count = await RoleService.update_menus(request=request, pk=pk, menu_ids=menu_ids) if count > 0: return await response_base.success() return await response_base.fail() -@router.delete('', summary='(批量)删除角色', dependencies=[DependsRBAC]) +@router.delete( + '', + summary='(批量)删除角色', + dependencies=[ + Depends(RequestPermission('sys:role:del')), + DependsRBAC, + ], +) async def delete_role(pk: Annotated[list[int], Query(...)]): count = await RoleService.delete(pk=pk) if count > 0: diff --git a/backend/app/api/v1/task.py b/backend/app/api/v1/task.py index 34cd8d36..2745a65f 100644 --- a/backend/app/api/v1/task.py +++ b/backend/app/api/v1/task.py @@ -2,9 +2,10 @@ # -*- coding: utf-8 -*- from typing import Annotated -from fastapi import APIRouter, Body, Path +from fastapi import APIRouter, Body, Depends, Path from backend.app.common.jwt import DependsJwtAuth +from backend.app.common.permission import RequestPermission from backend.app.common.rbac import DependsRBAC from backend.app.common.response.response_code import CustomResponseCode from backend.app.common.response.response_schema import response_base @@ -27,7 +28,14 @@ async def get_task_result(pk: str = Path(description='任务ID')): return await response_base.success(data=task.result) -@router.post('/{module}', summary='执行任务', dependencies=[DependsRBAC]) +@router.post( + '/{module}', + summary='执行任务', + dependencies=[ + Depends(RequestPermission('sys:task:run')), + DependsRBAC, + ], +) async def run_task( module: Annotated[str, Path(description='任务模块')], args: Annotated[list | None, Body()] = None, diff --git a/backend/app/api/v1/user.py b/backend/app/api/v1/user.py index 30c63309..2cfb8eb1 100644 --- a/backend/app/api/v1/user.py +++ b/backend/app/api/v1/user.py @@ -2,10 +2,11 @@ # -*- coding: utf-8 -*- from typing import Annotated -from fastapi import APIRouter, Query, Request +from fastapi import APIRouter, Depends, Query, Request from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import PageDepends, paging_data +from backend.app.common.pagination import DependsPagination, paging_data +from backend.app.common.permission import RequestPermission from backend.app.common.rbac import DependsRBAC from backend.app.common.response.response_schema import response_base from backend.app.database.db_mysql import CurrentSession @@ -32,8 +33,8 @@ async def user_register(obj: RegisterUser): @router.post('/add', summary='添加用户', dependencies=[DependsRBAC]) -async def add_user(obj: AddUser): - await UserService.add(obj=obj) +async def add_user(request: Request, obj: AddUser): + await UserService.add(request=request, obj=obj) current_user = await UserService.get_userinfo(username=obj.username) data = GetAllUserInfo(**await select_as_dict(current_user)) return await response_base.success(data=data) @@ -68,9 +69,16 @@ async def update_userinfo(request: Request, username: str, obj: UpdateUser): return await response_base.fail() -@router.put('/{username}/role', summary='更新用户角色', dependencies=[DependsRBAC]) +@router.put( + '/{username}/role', + summary='更新用户角色', + dependencies=[ + Depends(RequestPermission('sys:user:role:edit')), + DependsRBAC, + ], +) async def update_user_role(request: Request, username: str, obj: UpdateUserRole): - await UserService.update_role(request=request, username=username, obj=obj) + await UserService.update_roles(request=request, username=username, obj=obj) return await response_base.success() @@ -82,7 +90,14 @@ async def update_avatar(request: Request, username: str, avatar: Avatar): return await response_base.fail() -@router.get('', summary='(模糊条件)分页获取所有用户', dependencies=[DependsJwtAuth, PageDepends]) +@router.get( + '', + summary='(模糊条件)分页获取所有用户', + dependencies=[ + DependsJwtAuth, + DependsPagination, + ], +) async def get_all_users( db: CurrentSession, dept: Annotated[int | None, Query()] = None, @@ -131,10 +146,13 @@ async def multi_set(request: Request, pk: int): path='/{username}', summary='用户注销', description='用户注销 != 用户登出,注销之后用户将从数据库删除', - dependencies=[DependsRBAC], + dependencies=[ + Depends(RequestPermission('sys:user:del')), + DependsRBAC, + ], ) -async def delete_user(request: Request, username: str): - count = await UserService.delete(request=request, username=username) +async def delete_user(username: str): + count = await UserService.delete(username=username) if count > 0: return await response_base.success() return await response_base.fail() diff --git a/backend/app/common/enums.py b/backend/app/common/enums.py index 950b87d5..9176d051 100644 --- a/backend/app/common/enums.py +++ b/backend/app/common/enums.py @@ -50,6 +50,7 @@ class MethodType(StrEnum): PUT = 'PUT' DELETE = 'DELETE' PATCH = 'PATCH' + OPTIONS = 'OPTIONS' class LoginLogStatusType(IntEnum): diff --git a/backend/app/common/jwt.py b/backend/app/common/jwt.py index fd13297d..e23ce39b 100644 --- a/backend/app/common/jwt.py +++ b/backend/app/common/jwt.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta from asgiref.sync import sync_to_async from fastapi import Depends, Request -from fastapi.security import OAuth2PasswordBearer +from fastapi.security import HTTPBearer, OAuth2PasswordBearer from fastapi.security.utils import get_authorization_scheme_param from jose import jwt from passlib.context import CryptContext @@ -19,8 +19,12 @@ from backend.app.utils.timezone import timezone pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') +# Deprecated, may be enabled when oauth2 is actually integrated oauth2_schema = OAuth2PasswordBearer(tokenUrl=settings.TOKEN_URL_SWAGGER) +# JWT authorizes dependency injection +DependsJwtAuth = Depends(HTTPBearer()) + @sync_to_async def get_hash_password(password: str) -> str: @@ -209,8 +213,3 @@ def superuser_verify(request: Request) -> bool: if not request.user.is_staff: raise AuthorizationError(msg='此管理员已被禁止后台管理操作') return is_superuser - - -# JWT authorizes dependency injection, which can be used if the interface only -# needs to provide a token instead of RBAC permission control -DependsJwtAuth = Depends(oauth2_schema) diff --git a/backend/app/common/pagination.py b/backend/app/common/pagination.py index 5ae7074d..11969b93 100644 --- a/backend/app/common/pagination.py +++ b/backend/app/common/pagination.py @@ -84,4 +84,4 @@ async def paging_data(db: AsyncSession, select: Select, page_data_schema: Schema # 分页依赖注入 -PageDepends = Depends(pagination_ctx(_Page)) +DependsPagination = Depends(pagination_ctx(_Page)) diff --git a/backend/app/common/permission.py b/backend/app/common/permission.py new file mode 100644 index 00000000..a6e1d81b --- /dev/null +++ b/backend/app/common/permission.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import Request + +from backend.app.common.exception.errors import ServerError +from backend.app.core.conf import settings + + +class RequestPermission: + """ + 请求权限,仅用于角色菜单RBAC + + Tip: + 使用此请求权限时,需要将 `Depends(RequestPermission('xxx'))` 在 `DependsRBAC` 之前设置, + 因为 fastapi 当前版本的接口依赖注入按正序执行,意味着 RBAC 标识会在验证前被设置 + """ + + def __init__(self, value: str): + self.value = value + + async def __call__(self, request: Request): + if settings.PERMISSION_MODE == 'role-menu': + if not isinstance(self.value, str): + raise ServerError + # 附加权限标识 + request.state.permission = self.value diff --git a/backend/app/common/rbac.py b/backend/app/common/rbac.py index fe2a5a7f..b43c643b 100644 --- a/backend/app/common/rbac.py +++ b/backend/app/common/rbac.py @@ -5,13 +5,13 @@ import casbin_async_sqlalchemy_adapter from fastapi import Depends, Request -from backend.app.common.enums import StatusType +from backend.app.common.enums import StatusType, MethodType from backend.app.common.exception.errors import AuthorizationError, TokenError from backend.app.common.jwt import DependsJwtAuth +from backend.app.common.redis import redis_client from backend.app.core.conf import settings -from backend.app.core.path_conf import RBAC_MODEL_CONF from backend.app.database.db_mysql import async_engine -from backend.app.models.sys_casbin_rule import CasbinRule +from backend.app.models import CasbinRule class RBAC: @@ -22,28 +22,46 @@ class RBAC: :return: """ + # 规则数据作为死数据直接在方法内定义 + _CASBIN_RBAC_MODEL_CONF_TEXT = """ + [request_definition] + r = sub, obj, act + + [policy_definition] + p = sub, obj, act + + [role_definition] + g = _, _ + + [policy_effect] + e = some(where (p.eft == allow)) + + [matchers] + m = g(r.sub, p.sub) && (keyMatch(r.obj, p.obj) || keyMatch3(r.obj, p.obj)) && (r.act == p.act || p.act == "*") + """ adapter = casbin_async_sqlalchemy_adapter.Adapter(async_engine, db_class=CasbinRule) - enforcer = casbin.AsyncEnforcer(RBAC_MODEL_CONF, adapter) + model = casbin.AsyncEnforcer.new_model(text=_CASBIN_RBAC_MODEL_CONF_TEXT) + enforcer = casbin.AsyncEnforcer(model, adapter) await enforcer.load_policy() return enforcer - async def rbac_verify(self, request: Request, _: dict = DependsJwtAuth) -> None: + async def rbac_verify(self, request: Request, _token: str = DependsJwtAuth) -> None: """ RBAC 权限校验 :param request: - :param _: + :param _token: :return: """ path = request.url.path + # 鉴权白名单 if path in settings.TOKEN_EXCLUDE: return - # 强制校验 JWT 授权状态 + # JWT 授权状态强制校验 if not request.auth.scopes: raise TokenError # 超级管理员免校验 - super_user = request.user.is_superuser - if super_user: + if request.user.is_superuser: return # 检测角色数据权限范围 user_roles = request.user.roles @@ -51,44 +69,67 @@ class RBAC: raise AuthorizationError(msg='用户未分配角色,授权失败') if not any(len(role.menus) > 0 for role in user_roles): raise AuthorizationError(msg='用户所属角色未分配菜单,授权失败') - if not request.user.is_staff: - raise AuthorizationError(msg='此用户已被禁止后台管理操作') + method = request.method + if method != MethodType.GET or method != MethodType.OPTIONS: + if not request.user.is_staff: + raise AuthorizationError(msg='此用户已被禁止后台管理操作') + # 数据权限范围 data_scope = any(role.data_scope == 1 for role in user_roles) if data_scope: return - method = request.method - if settings.MENU_PERMISSION: - # 菜单权限校验 - # TODO: 改用流行方案,自定义接口权限字段标识 - path_auth = path.split(f'{settings.API_V1_STR}/')[-1].replace('/', ':') + f':{method}' - menu_perms = [] - forbid_menu_perms = [] - for role in user_roles: - if role.menus: - for menu in role.menus: - if menu.status == StatusType.enable: - menu_perms.append(menu.perms) - else: - forbid_menu_perms.append(menu.perms) - if path_auth in set(settings.MENU_EXCLUDE): + user_uuid = request.user.uuid + path_auth_perm = request.state.permission + if settings.PERMISSION_MODE == 'role-menu': + # 角色菜单权限校验 + if path_auth_perm in set(settings.ROLE_MENU_EXCLUDE): return - if path_auth in set([perm for perms_str in forbid_menu_perms for perm in perms_str.split(',')]): + user_menu_perms = await redis_client.get(f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:enable') + user_forbid_menu_perms = await redis_client.get(f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:disable') + if not user_menu_perms or not user_forbid_menu_perms: + user_menu_perms = [] + user_forbid_menu_perms = [] + for role in user_roles: + user_menus = role.menus + if user_menus: + for menu in user_menus: + perms = menu.perms + if perms: + if menu.status == StatusType.enable: + user_menu_perms.extend(perms.split(',')) + else: + user_forbid_menu_perms.extend(perms.split(',')) + await redis_client.set( + f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:enable', ','.join(user_menu_perms) + ) + await redis_client.set( + f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:disable', ','.join(user_forbid_menu_perms) + ) + if path_auth_perm in user_forbid_menu_perms: raise AuthorizationError(msg='菜单已禁用,授权失败') - if path_auth not in set([perm for perms_str in menu_perms for perm in perms_str.split(',')]): + if path_auth_perm not in user_menu_perms: raise AuthorizationError else: # casbin 权限校验 - forbid_menu_path = [] - for role in user_roles: - if role.menus: - for menu in role.menus: - if menu.status == StatusType.disable: - forbid_menu_path.append(menu.path) - if path.split('/')[-1] in forbid_menu_path: + user_forbid_menu_perms = await redis_client.get( + f'{settings.PERMISSION_REDIS_PREFIX}:{request.user.uuid}:disable' + ) + if not user_forbid_menu_perms: + user_forbid_menu_perms = [] + for role in user_roles: + user_menus = role.menus + if user_menus: + for menu in user_menus: + perms = menu.perms + if perms: + if menu.status == StatusType.disable: + user_forbid_menu_perms.extend(perms.split(',')) + await redis_client.set( + f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:disable', ','.join(user_forbid_menu_perms) + ) + if path_auth_perm in user_forbid_menu_perms: raise AuthorizationError(msg='菜单已禁用,授权失败') if (method, path) in settings.CASBIN_EXCLUDE: return - user_uuid = request.user.uuid enforcer = await self.enforcer() if not enforcer.enforce(user_uuid, path, method): raise AuthorizationError diff --git a/backend/app/core/conf.py b/backend/app/core/conf.py index b3ccd26b..c64d5d84 100644 --- a/backend/app/core/conf.py +++ b/backend/app/core/conf.py @@ -103,7 +103,7 @@ class Settings(BaseSettings): TOKEN_URL_SWAGGER: str = f'{API_V1_STR}/auth/swagger_login' TOKEN_REDIS_PREFIX: str = 'fba_token' TOKEN_REFRESH_REDIS_PREFIX: str = 'fba_refresh_token' - TOKEN_EXCLUDE: list[str] = [ # 白名单 + TOKEN_EXCLUDE: list[str] = [ # JWT / RBAC 白名单 f'{API_V1_STR}/auth/login', ] @@ -120,8 +120,11 @@ class Settings(BaseSettings): MIDDLEWARE_GZIP: bool = True MIDDLEWARE_ACCESS: bool = False - # Casbin - CASBIN_RBAC_MODEL_NAME: str = 'rbac_model.conf' + # RBAC Permission + PERMISSION_MODE: Literal['casbin', 'role-menu'] = 'casbin' + PERMISSION_REDIS_PREFIX: str = 'fba_permission' + + # Casbin Auth CASBIN_EXCLUDE: set[tuple[str, str]] = { ('POST', f'{API_V1_STR}/auth/swagger_login'), ('POST', f'{API_V1_STR}/auth/login'), @@ -130,14 +133,10 @@ class Settings(BaseSettings): ('GET', f'{API_V1_STR}/auth/captcha'), } - # Menu - MENU_PERMISSION: bool = False # 危险行为,开启此功能, Casbin 鉴权将失效,并将使用角色菜单鉴权 (默认关闭) - MENU_EXCLUDE: list[str] = [ - 'auth:swagger_login:post', - 'auth:login:post', - 'auth:logout:post', - 'auth:register:post', - 'auth:captcha:get', + # Role Menu Auth + ROLE_MENU_EXCLUDE: list[str] = [ + 'sys:monitor:redis', + 'sys:monitor:server', ] # Opera log diff --git a/backend/app/core/path_conf.py b/backend/app/core/path_conf.py index 0326e322..97cede49 100644 --- a/backend/app/core/path_conf.py +++ b/backend/app/core/path_conf.py @@ -4,8 +4,6 @@ import os from pathlib import Path -from backend.app.core.conf import settings - # 获取项目根目录 # 或使用绝对路径,指到backend目录为止,例如windows:BasePath = D:\git_project\fastapi_mysql\backend BasePath = Path(__file__).resolve().parent.parent.parent @@ -16,8 +14,5 @@ Versions = os.path.join(BasePath, 'app', 'alembic', 'versions') # 日志文件路径 LogPath = os.path.join(BasePath, 'app', 'log') -# RBAC model.conf 文件路径 -RBAC_MODEL_CONF = os.path.join(BasePath, 'app', 'core', settings.CASBIN_RBAC_MODEL_NAME) - # 离线 IP 数据库路径 IP2REGION_XDB = os.path.join(BasePath, 'app', 'static', 'ip2region.xdb') diff --git a/backend/app/core/rbac_model.conf b/backend/app/core/rbac_model.conf deleted file mode 100644 index 43dbcfb6..00000000 --- a/backend/app/core/rbac_model.conf +++ /dev/null @@ -1,14 +0,0 @@ -[request_definition] -r = sub, obj, act - -[policy_definition] -p = sub, obj, act - -[role_definition] -g = _, _ - -[policy_effect] -e = some(where (p.eft == allow)) - -[matchers] -m = g(r.sub, p.sub) && (keyMatch(r.obj, p.obj) || keyMatch3(r.obj, p.obj)) && (r.act == p.act || p.act == "*") diff --git a/backend/app/crud/crud_casbin.py b/backend/app/crud/crud_casbin.py index 022221a4..3a3287e8 100644 --- a/backend/app/crud/crud_casbin.py +++ b/backend/app/crud/crud_casbin.py @@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from backend.app.crud.base import CRUDBase from backend.app.models import CasbinRule -from backend.app.schemas.casbin_rule import CreatePolicy, DeleteAllPolicies, DeleteAllUserRoles, UpdatePolicy +from backend.app.schemas.casbin_rule import CreatePolicy, DeleteAllPolicies, UpdatePolicy class CRUDCasbin(CRUDBase[CasbinRule, CreatePolicy, UpdatePolicy]): @@ -28,8 +28,8 @@ class CRUDCasbin(CRUDBase[CasbinRule, CreatePolicy, UpdatePolicy]): result = await db.execute(delete(self.model).where(or_(*where_list))) return result.rowcount - async def delete_groups_by_uuid(self, db: AsyncSession, sub: DeleteAllUserRoles) -> int: - result = await db.execute(delete(self.model).where(self.model.v0 == sub.uuid)) + async def delete_groups_by_uuid(self, db: AsyncSession, uuid: str) -> int: + result = await db.execute(delete(self.model).where(self.model.v0 == uuid)) return result.rowcount diff --git a/backend/app/crud/crud_menu.py b/backend/app/crud/crud_menu.py index e031dfde..9b6fc55c 100644 --- a/backend/app/crud/crud_menu.py +++ b/backend/app/crud/crud_menu.py @@ -15,7 +15,7 @@ class CRUDMenu(CRUDBase[Menu, CreateMenu, UpdateMenu]): return await self.get_(db, pk=menu_id) async def get_by_title(self, db, title: str) -> Menu | None: - result = await db.execute(select(self.model).where(self.model.title == title)) + result = await db.execute(select(self.model).where(and_(self.model.title == title, self.model.menu_type != 2))) return result.scalars().first() async def get_all(self, db, title: str | None = None, status: int | None = None) -> Sequence[Menu]: @@ -43,7 +43,8 @@ class CRUDMenu(CRUDBase[Menu, CreateMenu, UpdateMenu]): await self.create_(db, obj_in) async def update(self, db, menu_id: int, obj_in: UpdateMenu) -> int: - return await self.update_(db, menu_id, obj_in) + count = await self.update_(db, menu_id, obj_in) + return count async def delete(self, db, menu_id: int) -> int: return await self.delete_(db, menu_id) diff --git a/backend/app/crud/crud_user.py b/backend/app/crud/crud_user.py index 776d5a72..5e844e57 100644 --- a/backend/app/crud/crud_user.py +++ b/backend/app/crud/crud_user.py @@ -44,7 +44,7 @@ class CRUDUser(CRUDBase[User, RegisterUser, UpdateUser]): async def add(self, db: AsyncSession, obj: AddUser) -> None: salt = text_captcha(5) obj.password = await jwt.get_hash_password(obj.password + salt) - dict_obj = obj.dict(exclude={'roles'}) + dict_obj = obj.model_dump(exclude={'roles'}) dict_obj.update({'salt': salt}) new_user = self.model(**dict_obj) role_list = [] diff --git a/backend/app/models/sys_casbin_rule.py b/backend/app/models/sys_casbin_rule.py index 0d71e7c9..ec7fb96a 100644 --- a/backend/app/models/sys_casbin_rule.py +++ b/backend/app/models/sys_casbin_rule.py @@ -8,13 +8,13 @@ from backend.app.models.base import MappedBase, id_key class CasbinRule(MappedBase): - """重写 casbin 中的 casbinRule model 类, 使用自定义 Base, 避免产生 alembic 迁移问题""" + """重写 casbin 中的 CasbinRule model 类, 使用自定义 Base, 避免产生 alembic 迁移问题""" __tablename__ = 'sys_casbin_rule' id: Mapped[id_key] - ptype: Mapped[str] = mapped_column(String(255), comment='策略类型: p 或者 g') - v0: Mapped[str] = mapped_column(String(255), comment='角色 / 用户uuid') + ptype: Mapped[str] = mapped_column(String(255), comment='策略类型: p / g') + v0: Mapped[str] = mapped_column(String(255), comment='角色ID / 用户uuid') v1: Mapped[str] = mapped_column(LONGTEXT, comment='api路径 / 角色名称') v2: Mapped[str | None] = mapped_column(String(255), comment='请求方法') v3: Mapped[str | None] = mapped_column(String(255)) diff --git a/backend/app/models/sys_menu.py b/backend/app/models/sys_menu.py index f2b6f1db..8c4aebca 100644 --- a/backend/app/models/sys_menu.py +++ b/backend/app/models/sys_menu.py @@ -16,7 +16,7 @@ class Menu(Base): __tablename__ = 'sys_menu' id: Mapped[id_key] = mapped_column(init=False) - title: Mapped[str] = mapped_column(String(50), unique=True, comment='菜单标题') + title: Mapped[str] = mapped_column(String(50), comment='菜单标题') name: Mapped[str] = mapped_column(String(50), comment='菜单名称') level: Mapped[int] = mapped_column(default=0, comment='菜单层级') sort: Mapped[int] = mapped_column(default=0, comment='排序') diff --git a/backend/app/schemas/casbin_rule.py b/backend/app/schemas/casbin_rule.py index 28f03efc..43c2ea7e 100644 --- a/backend/app/schemas/casbin_rule.py +++ b/backend/app/schemas/casbin_rule.py @@ -7,7 +7,7 @@ from backend.app.schemas.base import SchemaBase class CreatePolicy(SchemaBase): - sub: str = Field(..., description='用户uuid / 角色') + sub: str = Field(..., description='用户uuid / 角色ID') path: str = Field(..., description='api 路径') method: MethodType = Field(default=MethodType.GET, description='请求方法') @@ -41,15 +41,11 @@ class DeleteUserRole(CreateUserRole): pass -class DeleteAllUserRoles(SchemaBase): - uuid: str - - class GetAllPolicy(SchemaBase): model_config = ConfigDict(from_attributes=True) id: int - ptype: str = Field(..., description='规则类型, p 或 g') + ptype: str = Field(..., description='规则类型, p / g') v0: str = Field(..., description='用户 uuid / 角色') v1: str = Field(..., description='api 路径 / 角色') v2: str | None = None diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index f4d63d81..f7ae64f3 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -76,6 +76,9 @@ class GetAllUserInfo(GetUserInfoNoRelation): class GetCurrentUserInfo(GetAllUserInfo): model_config = ConfigDict(from_attributes=True) + dept: GetAllDept | str | None = None + roles: list[GetAllRole] | list[str] | None = None + @model_validator(mode='after') def handel(self, values): """处理部门和角色""" diff --git a/backend/app/services/casbin_service.py b/backend/app/services/casbin_service.py index 6b884dd7..e4c63673 100644 --- a/backend/app/services/casbin_service.py +++ b/backend/app/services/casbin_service.py @@ -10,7 +10,6 @@ from backend.app.schemas.casbin_rule import ( CreatePolicy, CreateUserRole, DeleteAllPolicies, - DeleteAllUserRoles, DeletePolicy, DeleteUserRole, UpdatePolicy, @@ -130,7 +129,7 @@ class CasbinService: return data @staticmethod - async def delete_all_groups(*, uuid: DeleteAllUserRoles) -> int: + async def delete_all_groups(*, uuid: str) -> int: async with async_db_session.begin() as db: count = await CasbinDao.delete_groups_by_uuid(db, uuid) return count diff --git a/backend/app/services/menu_service.py b/backend/app/services/menu_service.py index 69b5fe4c..f7c1fb90 100644 --- a/backend/app/services/menu_service.py +++ b/backend/app/services/menu_service.py @@ -5,6 +5,8 @@ from typing import Any from fastapi import Request from backend.app.common.exception import errors +from backend.app.common.redis import redis_client +from backend.app.core.conf import settings from backend.app.crud.crud_menu import MenuDao from backend.app.crud.crud_role import RoleDao from backend.app.database.db_mysql import async_db_session @@ -79,6 +81,7 @@ class MenuService: if not parent_menu: raise errors.NotFoundError(msg='父级菜单不存在') count = await MenuDao.update(db, pk, obj) + await redis_client.delete_prefix(settings.PERMISSION_REDIS_PREFIX) return count @staticmethod diff --git a/backend/app/services/role_service.py b/backend/app/services/role_service.py index 91c5cb3d..115312a5 100644 --- a/backend/app/services/role_service.py +++ b/backend/app/services/role_service.py @@ -2,9 +2,12 @@ # -*- coding: utf-8 -*- from typing import Sequence +from fastapi import Request from sqlalchemy import Select from backend.app.common.exception import errors +from backend.app.common.redis import redis_client +from backend.app.core.conf import settings from backend.app.crud.crud_menu import MenuDao from backend.app.crud.crud_role import RoleDao from backend.app.database.db_mysql import async_db_session @@ -59,7 +62,7 @@ class RoleService: return count @staticmethod - async def update_menus(*, pk: int, menu_ids: UpdateRoleMenu) -> int: + async def update_menus(*, request: Request, pk: int, menu_ids: UpdateRoleMenu) -> int: async with async_db_session.begin() as db: role = await RoleDao.get(db, pk) if not role: @@ -69,6 +72,7 @@ class RoleService: if not menu: raise errors.NotFoundError(msg='菜单不存在') count = await RoleDao.update_menus(db, pk, menu_ids) + await redis_client.delete_prefix(f'{settings.PERMISSION_REDIS_PREFIX}:{request.user.uuid}') return count @staticmethod diff --git a/backend/app/services/user_service.py b/backend/app/services/user_service.py index b5ae0ad9..7366d1cc 100644 --- a/backend/app/services/user_service.py +++ b/backend/app/services/user_service.py @@ -34,8 +34,9 @@ class UserService: await UserDao.create(db, obj) @staticmethod - async def add(*, obj: AddUser) -> None: + async def add(*, request: Request, obj: AddUser) -> None: async with async_db_session.begin() as db: + await superuser_verify(request) username = await UserDao.get_by_username(db, obj.username) if username: raise errors.ForbiddenError(msg='此用户名已注册') @@ -85,7 +86,9 @@ class UserService: @staticmethod async def update(*, request: Request, username: str, obj: UpdateUser) -> int: async with async_db_session.begin() as db: - await superuser_verify(request) + if not request.user.is_superuser: + if request.user.username != username: + raise errors.ForbiddenError(msg='你只能修改自己的信息') input_user = await UserDao.get_with_relation(db, username=username) if not input_user: raise errors.NotFoundError(msg='用户不存在') @@ -105,7 +108,7 @@ class UserService: return count @staticmethod - async def update_role(*, request: Request, username: str, obj: UpdateUserRole) -> None: + async def update_roles(*, request: Request, username: str, obj: UpdateUserRole) -> None: async with async_db_session.begin() as db: if not request.user.is_superuser: if request.user.username != username: @@ -118,6 +121,7 @@ class UserService: if not role: raise errors.NotFoundError(msg='角色不存在') await UserDao.update_role(db, input_user, obj) + await redis_client.delete_prefix(f'{settings.PERMISSION_REDIS_PREFIX}:{request.user.uuid}') @staticmethod async def update_avatar(*, request: Request, username: str, avatar: Avatar) -> int: @@ -196,9 +200,8 @@ class UserService: return count @staticmethod - async def delete(*, request: Request, username: str) -> int: + async def delete(*, username: str) -> int: async with async_db_session.begin() as db: - await superuser_verify(request) input_user = await UserDao.get_by_username(db, username) if not input_user: raise errors.NotFoundError(msg='用户不存在') diff --git a/backend/sql/create_tables.sql b/backend/sql/create_tables.sql index 5d04ae76..572d0371 100644 --- a/backend/sql/create_tables.sql +++ b/backend/sql/create_tables.sql @@ -114,8 +114,7 @@ CREATE TABLE sys_menu created_time DATETIME NOT NULL COMMENT '创建时间', updated_time DATETIME COMMENT '更新时间', PRIMARY KEY (id), - FOREIGN KEY (parent_id) REFERENCES sys_menu (id) ON DELETE SET NULL, - UNIQUE (title) + FOREIGN KEY (parent_id) REFERENCES sys_menu (id) ON DELETE SET NULL ); CREATE INDEX ix_sys_menu_id ON sys_menu (id); diff --git a/backend/sql/init_pytest_data.sql b/backend/sql/init_pytest_data.sql index e9c99c73..81a12002 100644 --- a/backend/sql/init_pytest_data.sql +++ b/backend/sql/init_pytest_data.sql @@ -2,7 +2,7 @@ INSERT INTO fba_test.sys_dept (id, name, level, sort, leader, phone, email, stat VALUES (1, 'test', 0, 0, null, null, null, 1, 0, null, '2023-06-26 17:13:45', null); INSERT INTO fba_test.sys_menu (id, title, name, level, sort, icon, path, menu_type, component, perms, status, `show`, cache, remark, parent_id, created_time, updated_time) -VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null, '2023-07-27 19:14:10', '2023-07-27 19:14:52'), +VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null, '2023-07-27 19:14:10', null), (2, '仪表盘', 'dashboard', 0, 0, 'IconDashboard', 'dashboard', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:15:45', null), (3, '工作台', 'Workplace', 0, 0, null, 'workplace', 1, '/dashboard/workplace/index.vue', null, 1, 1, 1, null, 2, '2023-07-27 19:17:59', null), (4, 'arco官网', 'arcoWebsite', 0, 888, 'IconLink', 'https://arco.design', 1, null, null, 1, 1, 1, null, null, '2023-07-27 19:19:23', null), @@ -12,13 +12,28 @@ VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null (8, '常见问题', 'faq', 0, 999, 'IconQuestion', 'https://arco.design/vue/docs/pro/faq', 1, null, null, 1, 1, 1, null, null, '2023-07-27 19:22:24', null), (9, '系统管理', 'admin', 0, 6, 'IconSettings', 'admin', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:23:00', null), (10, '部门管理', 'SysDept', 0, 0, null, 'sys-dept', 1, '/admin/dept/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:23:42', null), - (11, 'API管理', 'SysApi', 0, 1, null, 'sys-api', 1, '/admin/api/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:24:12', null), - (12, '用户管理', 'SysUser', 0, 0, null, 'sys-user', 1, '/admin/user/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:13', null), - (13, '角色管理', 'SysRole', 0, 2, null, 'sys-role', 1, '/admin/role/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:45', null), - (14, '菜单管理', 'SysMenu', 0, 2, null, 'sys-menu', 1, '/admin/menu/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:45:29', null), - (15, '系统监控', 'monitor', 0, 88, 'IconComputer', 'monitor', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:27:08', null), - (16, 'Redis监控', 'Redis', 0, 0, null, 'redis', 1, '/monitor/redis/index.vue', null, 1, 1, 1, null, 15, '2023-07-27 19:28:03', null), - (17, '服务器监控', 'Server', 0, 0, null, 'server', 1, '/monitor/server/index.vue', null, 1, 1, 1, null, 15, '2023-07-27 19:28:29', null); + (11, '新增', '', 0, 0, null, null, 2, null, 'sys:dept:add', 1, 1, 1, null, 10, '2024-01-07 11:37:00', null), + (12, '编辑', '', 0, 0, null, null, 2, null, 'sys:dept:edit', 1, 1, 1, null, 10, '2024-01-07 11:37:29', null), + (13, '删除', '', 0, 0, null, null, 2, null, 'sys:dept:del', 1, 1, 1, null, 10, '2024-01-07 11:37:44', null), + (14, 'API管理', 'SysApi', 0, 1, null, 'sys-api', 1, '/admin/api/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:24:12', null), + (15, '新增', '', 0, 0, null, null, 2, null, 'sys:api:add', 1, 1, 1, null, 14, '2024-01-07 11:57:09', null), + (16, '编辑', '', 0, 0, null, null, 2, null, 'sys:api:edit', 1, 1, 1, null, 14, '2024-01-07 11:57:44', null), + (17, '删除', '', 0, 0, null, null, 2, null, 'sys:api:del', 1, 1, 1, null, 14, '2024-01-07 11:57:56', null), + (18, '用户管理', 'SysUser', 0, 0, null, 'sys-user', 1, '/admin/user/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:13', null), + (19, '编辑用户角色', '', 0, 0, null, null, 2, null, 'sys:user:role:edit', 1, 1, 1, null, 18, '2024-01-07 12:04:20', null), + (20, '注销', '', 0, 0, null, null, 2, null, 'sys:user:del', 1, 1, 1, '用户注销 != 用户登出,注销之后用户将从数据库删除', 18, '2024-01-07 02:28:09', null), + (21, '角色管理', 'SysRole', 0, 2, null, 'sys-role', 1, '/admin/role/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:45', null), + (22, '新增', '', 0, 0, null, null, 2, null, 'sys:role:add', 1, 1, 1, null, 21, '2024-01-07 11:58:37', null), + (23, '编辑', '', 0, 0, null, null, 2, null, 'sys:role:edit', 1, 1, 1, null, 21, '2024-01-07 11:58:52', null), + (24, '删除', '', 0, 0, null, null, 2, null, 'sys:role:del', 1, 1, 1, null, 21, '2024-01-07 11:59:07', null), + (25, '编辑角色菜单', '', 0, 0, null, null, 2, null, 'sys:role:menu:edit', 1, 1, 1, null, 21, '2024-01-07 01:59:39', null), + (26, '菜单管理', 'SysMenu', 0, 2, null, 'sys-menu', 1, '/admin/menu/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:45:29', null), + (27, '新增', '', 0, 0, null, null, 2, null, 'sys:menu:add', 1, 1, 1, null, 26, '2024-01-07 12:01:24', null), + (28, '编辑', '', 0, 0, null, null, 2, null, 'sys:menu:edit', 1, 1, 1, null, 26, '2024-01-07 12:01:34', null), + (29, '删除', '', 0, 0, null, null, 2, null, 'sys:menu:del', 1, 1, 1, null, 26, '2024-01-07 12:01:48', null), + (30, '系统监控', 'monitor', 0, 88, 'IconComputer', 'monitor', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:27:08', null), + (31, 'Redis监控', 'Redis', 0, 0, null, 'redis', 1, '/monitor/redis/index.vue', 'sys:monitor:redis', 1, 1, 1, null, 30, '2023-07-27 19:28:03', null), + (32, '服务器监控', 'Server', 0, 0, null, 'server', 1, '/monitor/server/index.vue', 'sys:monitor:server', 1, 1, 1, null, 30, '2023-07-27 19:28:29', null); INSERT INTO fba_test.sys_role (id, name, data_scope, status, remark, created_time, updated_time) VALUES (1, 'test', 2, 1, null, '2023-06-26 17:13:45', null); diff --git a/backend/sql/init_test_data.sql b/backend/sql/init_test_data.sql index a6d17806..d13082c4 100644 --- a/backend/sql/init_test_data.sql +++ b/backend/sql/init_test_data.sql @@ -2,7 +2,7 @@ INSERT INTO fba.sys_dept (id, name, level, sort, leader, phone, email, status, d VALUES (1, 'test', 0, 0, null, null, null, 1, 0, null, '2023-06-26 17:13:45', null); INSERT INTO fba.sys_menu (id, title, name, level, sort, icon, path, menu_type, component, perms, status, `show`, cache, remark, parent_id, created_time, updated_time) -VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null, '2023-07-27 19:14:10', '2023-07-27 19:14:52'), +VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null, '2023-07-27 19:14:10', null), (2, '仪表盘', 'dashboard', 0, 0, 'IconDashboard', 'dashboard', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:15:45', null), (3, '工作台', 'Workplace', 0, 0, null, 'workplace', 1, '/dashboard/workplace/index.vue', null, 1, 1, 1, null, 2, '2023-07-27 19:17:59', null), (4, 'arco官网', 'arcoWebsite', 0, 888, 'IconLink', 'https://arco.design', 1, null, null, 1, 1, 1, null, null, '2023-07-27 19:19:23', null), @@ -12,13 +12,28 @@ VALUES (1, '测试', 'test', 0, 0, '', null, 0, null, null, 0, 0, 1, null, null (8, '常见问题', 'faq', 0, 999, 'IconQuestion', 'https://arco.design/vue/docs/pro/faq', 1, null, null, 1, 1, 1, null, null, '2023-07-27 19:22:24', null), (9, '系统管理', 'admin', 0, 6, 'IconSettings', 'admin', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:23:00', null), (10, '部门管理', 'SysDept', 0, 0, null, 'sys-dept', 1, '/admin/dept/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:23:42', null), - (11, 'API管理', 'SysApi', 0, 1, null, 'sys-api', 1, '/admin/api/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:24:12', null), - (12, '用户管理', 'SysUser', 0, 0, null, 'sys-user', 1, '/admin/user/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:13', null), - (13, '角色管理', 'SysRole', 0, 2, null, 'sys-role', 1, '/admin/role/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:45', null), - (14, '菜单管理', 'SysMenu', 0, 2, null, 'sys-menu', 1, '/admin/menu/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:45:29', null), - (15, '系统监控', 'monitor', 0, 88, 'IconComputer', 'monitor', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:27:08', null), - (16, 'Redis监控', 'Redis', 0, 0, null, 'redis', 1, '/monitor/redis/index.vue', null, 1, 1, 1, null, 15, '2023-07-27 19:28:03', null), - (17, '服务器监控', 'Server', 0, 0, null, 'server', 1, '/monitor/server/index.vue', null, 1, 1, 1, null, 15, '2023-07-27 19:28:29', null); + (11, '新增', '', 0, 0, null, null, 2, null, 'sys:dept:add', 1, 1, 1, null, 10, '2024-01-07 11:37:00', null), + (12, '编辑', '', 0, 0, null, null, 2, null, 'sys:dept:edit', 1, 1, 1, null, 10, '2024-01-07 11:37:29', null), + (13, '删除', '', 0, 0, null, null, 2, null, 'sys:dept:del', 1, 1, 1, null, 10, '2024-01-07 11:37:44', null), + (14, 'API管理', 'SysApi', 0, 1, null, 'sys-api', 1, '/admin/api/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:24:12', null), + (15, '新增', '', 0, 0, null, null, 2, null, 'sys:api:add', 1, 1, 1, null, 14, '2024-01-07 11:57:09', null), + (16, '编辑', '', 0, 0, null, null, 2, null, 'sys:api:edit', 1, 1, 1, null, 14, '2024-01-07 11:57:44', null), + (17, '删除', '', 0, 0, null, null, 2, null, 'sys:api:del', 1, 1, 1, null, 14, '2024-01-07 11:57:56', null), + (18, '用户管理', 'SysUser', 0, 0, null, 'sys-user', 1, '/admin/user/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:13', null), + (19, '编辑用户角色', '', 0, 0, null, null, 2, null, 'sys:user:role:edit', 1, 1, 1, null, 18, '2024-01-07 12:04:20', null), + (20, '注销', '', 0, 0, null, null, 2, null, 'sys:user:del', 1, 1, 1, '用户注销 != 用户登出,注销之后用户将从数据库删除', 18, '2024-01-07 02:28:09', null), + (21, '角色管理', 'SysRole', 0, 2, null, 'sys-role', 1, '/admin/role/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:25:45', null), + (22, '新增', '', 0, 0, null, null, 2, null, 'sys:role:add', 1, 1, 1, null, 21, '2024-01-07 11:58:37', null), + (23, '编辑', '', 0, 0, null, null, 2, null, 'sys:role:edit', 1, 1, 1, null, 21, '2024-01-07 11:58:52', null), + (24, '删除', '', 0, 0, null, null, 2, null, 'sys:role:del', 1, 1, 1, null, 21, '2024-01-07 11:59:07', null), + (25, '编辑角色菜单', '', 0, 0, null, null, 2, null, 'sys:role:menu:edit', 1, 1, 1, null, 21, '2024-01-07 01:59:39', null), + (26, '菜单管理', 'SysMenu', 0, 2, null, 'sys-menu', 1, '/admin/menu/index.vue', null, 1, 1, 1, null, 9, '2023-07-27 19:45:29', null), + (27, '新增', '', 0, 0, null, null, 2, null, 'sys:menu:add', 1, 1, 1, null, 26, '2024-01-07 12:01:24', null), + (28, '编辑', '', 0, 0, null, null, 2, null, 'sys:menu:edit', 1, 1, 1, null, 26, '2024-01-07 12:01:34', null), + (29, '删除', '', 0, 0, null, null, 2, null, 'sys:menu:del', 1, 1, 1, null, 26, '2024-01-07 12:01:48', null), + (30, '系统监控', 'monitor', 0, 88, 'IconComputer', 'monitor', 0, null, null, 1, 1, 1, null, null, '2023-07-27 19:27:08', null), + (31, 'Redis监控', 'Redis', 0, 0, null, 'redis', 1, '/monitor/redis/index.vue', 'sys:monitor:redis', 1, 1, 1, null, 30, '2023-07-27 19:28:03', null), + (32, '服务器监控', 'Server', 0, 0, null, 'server', 1, '/monitor/server/index.vue', 'sys:monitor:server', 1, 1, 1, null, 30, '2023-07-27 19:28:29', null); INSERT INTO fba.sys_role (id, name, data_scope, status, remark, created_time, updated_time) VALUES (1, 'test', 2, 1, null, '2023-06-26 17:13:45', null); diff --git a/requirements.txt b/requirements.txt index 8b36e646..33b0e02e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,14 +4,14 @@ alembic==1.13.0 asgiref==3.7.2 asyncmy==0.2.9 bcrypt==4.0.1 -casbin==1.33.0 -casbin-async-sqlalchemy-adapter==1.3.0 +casbin==1.34.0 +casbin-async-sqlalchemy-adapter==1.4.0 celery==5.3.6 cryptography==41.0.7 email-validator==2.0.0 fast-captcha==0.2.1 fastapi==0.108.0 -fastapi-limiter==0.1.5 +fastapi-limiter==0.1.6 fastapi-pagination==0.12.13 gunicorn==21.2.0 httpx==0.25.2 @@ -30,7 +30,7 @@ pytest-pretty==1.2.0 python-jose==3.3.0 python-multipart==0.0.6 pytz==2023.3 -redis[hiredis]==4.5.5 +redis[hiredis]==5.0.1 ruff==0.1.8 SQLAlchemy==2.0.23 supervisor==4.2.5