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