From 7f18df39ff69e270ed0672fe48bff1418a6b190e Mon Sep 17 00:00:00 2001 From: NoahLan <6995syu@163.com> Date: Tue, 19 May 2026 03:01:35 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E5=AF=BC=E5=87=BA=E6=A8=A1=E6=9D=BF=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iti_system/routes/menu.py | 36 ++++++++++++++---- iti_system/services.py | 31 +++++++++++++++- tests/test_integration.py | 78 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 134 insertions(+), 11 deletions(-) diff --git a/iti_system/routes/menu.py b/iti_system/routes/menu.py index a7f895f..c7163fe 100644 --- a/iti_system/routes/menu.py +++ b/iti_system/routes/menu.py @@ -8,9 +8,10 @@ from iti.auth import require_permission from iti.db import get_db from iti.responses import ok +from iti_system.enums import MenuTypeEnum, StatusEnum from iti_system.models import SysMenu from iti_system.schemas import MenuCreate, MenuUpdate -from iti_system.services import build_tree, dump_menu, get_or_404, new_id +from iti_system.services import build_tree, current_user, dump_menu, get_or_404, new_id, visible_menu_tree_for_user router = APIRouter(prefix="/sys/menu", tags=["system.menu"]) @@ -22,10 +23,17 @@ def list_menu(db: Session = Depends(get_db)): return ok([dump_menu(menu) for menu in menus]) -@router.get("/tree", dependencies=[Depends(require_permission("system:menu:list"))]) -def tree_menu(db: Session = Depends(get_db)): - menus = db.scalars(select(SysMenu).order_by(SysMenu.sort, SysMenu.name)).all() - return ok(build_tree(menus, dump_menu)) +@router.get("/tree") +def tree_menu(user=Depends(current_user), db: Session = Depends(get_db)): + menus = db.scalars( + select(SysMenu) + .where( + SysMenu.status == StatusEnum.ENABLED.value, + SysMenu.type != MenuTypeEnum.BUTTON.value, + ) + .order_by(SysMenu.sort, SysMenu.name) + ).all() + return ok(visible_menu_tree_for_user(user, menus)) @router.post("", dependencies=[Depends(require_permission("system:menu:create"))]) @@ -56,10 +64,22 @@ def delete_menu(id: str, db: Session = Depends(get_db)): @router.get("/exists", dependencies=[Depends(require_permission("system:menu:list"))]) -def exists_menu(name: str | None = None, auth_code: str | None = None, db: Session = Depends(get_db)): +def exists_menu( + id: str | None = None, + name: str | None = None, + path: str | None = None, + auth_code: str | None = None, + authCode: str | None = None, + db: Session = Depends(get_db), +): stmt = select(SysMenu) + if id: + stmt = stmt.where(SysMenu.id != id) if name: stmt = stmt.where(SysMenu.name == name) - if auth_code: - stmt = stmt.where(SysMenu.auth_code == auth_code) + if path: + stmt = stmt.where(SysMenu.path == path) + code = auth_code or authCode + if code: + stmt = stmt.where(SysMenu.auth_code == code) return ok({"exists": db.scalar(stmt) is not None}) diff --git a/iti_system/services.py b/iti_system/services.py index 0c1365b..8906ef4 100644 --- a/iti_system/services.py +++ b/iti_system/services.py @@ -21,7 +21,7 @@ from iti.db import get_db from iti.exceptions import BizError, Unauthorized from iti.responses import pagination -from iti_system.enums import StatusEnum +from iti_system.enums import MenuTypeEnum, StatusEnum from iti_system.models import ( Role, SysConfig, @@ -100,6 +100,7 @@ def get_or_404(db: Session, model, item_id: str, message: str = "数据不存在 def dump_user(user: User) -> dict[str, Any]: + role_codes = user.role_codes return { "id": user.id, "username": user.username, @@ -111,8 +112,10 @@ def dump_user(user: User) -> dict[str, Any]: "gender": user.gender, "status": user.status, "roles": [role.id for role in user.roles], + "roleCodes": role_codes, "depts": user.dept_ids, "permissions": user.permissions, + "isSuper": "ADMIN" in role_codes, "attributes": user.attribute_map, "createdAt": user.created_at, "updatedAt": user.updated_at, @@ -178,6 +181,32 @@ def build_tree(items: Iterable[Any], dumper) -> list[dict[str, Any]]: ] +def visible_menu_tree_for_user(user: User, menus: Iterable[SysMenu]) -> list[dict[str, Any]]: + visible_menus = [menu for menu in menus if menu.status == StatusEnum.ENABLED.value and menu.type != MenuTypeEnum.BUTTON.value] + if "ADMIN" in user.role_codes: + return build_tree(visible_menus, dump_menu) + + direct_menu_ids = { + menu.id + for role in user.roles + for menu in role.menus + if menu.status == StatusEnum.ENABLED.value + } + permissions = set(user.permissions) + menu_by_id = {menu.id: menu for menu in visible_menus} + visible_ids: set[str] = set() + + for menu in visible_menus: + if menu.id not in direct_menu_ids and (not menu.auth_code or menu.auth_code not in permissions): + continue + current: SysMenu | None = menu + while current is not None and current.id not in visible_ids: + visible_ids.add(current.id) + current = menu_by_id.get(current.parent_id) + + return build_tree([menu for menu in visible_menus if menu.id in visible_ids], dump_menu) + + def dump_dept(dept: SysDept, *, with_children: bool = False) -> dict[str, Any]: data = { "id": dept.id, diff --git a/tests/test_integration.py b/tests/test_integration.py index 189b318..d5511f0 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,9 +1,11 @@ from fastapi.testclient import TestClient from iti import create_app +from iti.auth import create_access_token from iti.config import BaseConfig from iti.db import Base from iti_system import create_system_module +from iti_system.models import Role, SysMenu, User from iti_system.seeds import seed_system_data @@ -67,10 +69,13 @@ def test_login_and_current_user_flow(): response = client.get("/sys/user/current", headers=headers) assert response.status_code == 200 - assert response.json()["data"]["username"] == "admin" + data = response.json()["data"] + assert data["username"] == "admin" + assert data["roleCodes"] == ["ADMIN"] + assert data["isSuper"] is True event = app.state.audit_dispatcher._queue.get_nowait() assert event.type == "login" - assert event.actor_id == response.json()["data"]["id"] + assert event.actor_id == data["id"] assert event.success is True @@ -83,6 +88,75 @@ def test_admin_can_list_system_routes(): assert client.get("/sys/config/list", headers=headers).json()["code"] == 200 +def test_menu_exists_supports_path_auth_code_and_excludes_current_id(): + client = TestClient(make_app()) + headers = login(client) + + by_path = client.get("/sys/menu/exists", params={"path": "/system/menu"}, headers=headers) + same_path = client.get( + "/sys/menu/exists", + params={"id": "system-menu", "path": "/system/menu"}, + headers=headers, + ) + by_auth_code = client.get( + "/sys/menu/exists", + params={"authCode": "system:menu:list"}, + headers=headers, + ) + + assert by_path.json()["data"] == {"exists": True} + assert same_path.json()["data"] == {"exists": False} + assert by_auth_code.json()["data"] == {"exists": True} + + +def test_menu_tree_returns_current_user_visible_routes_without_menu_admin_permission(): + app = make_app() + with app.state.db_sessionmaker() as db: + parent = SysMenu( + id="orders", + name="Orders", + type="catalog", + path="/orders", + status="enabled", + ) + child = SysMenu( + id="orders-list", + name="OrdersList", + type="menu", + path="/orders/list", + component="/orders/list", + auth_code="orders:list", + parent=parent, + status="enabled", + ) + button = SysMenu( + id="orders-create", + name="OrdersCreate", + type="button", + auth_code="orders:create", + parent=child, + status="enabled", + ) + role = Role(name="操作员", code="OPERATOR", menus=[child, button]) + user = User(username="operator", status="enabled", roles=[role]) + user.set_password("123456") + db.add_all([parent, child, button, role, user]) + db.commit() + user_id = user.id + + token = create_access_token(user_id, app.state.config) + response = TestClient(app).get( + "/sys/menu/tree", + headers={"Authorization": f"Bearer {token}"}, + ) + + assert response.status_code == 200 + data = response.json()["data"] + assert [item["id"] for item in data] == ["orders"] + assert [item["id"] for item in data[0]["children"]] == ["orders-list"] + assert data[0]["children"][0]["children"] == [] + + def test_internal_audit_writes_sys_log(): client = TestClient(make_app())