refactor: 重构导入导出模板相关功能

main
NoahLan 1 week ago
parent 631e06f70b
commit 7f18df39ff

@ -8,9 +8,10 @@ from iti.auth import require_permission
from iti.db import get_db from iti.db import get_db
from iti.responses import ok from iti.responses import ok
from iti_system.enums import MenuTypeEnum, StatusEnum
from iti_system.models import SysMenu from iti_system.models import SysMenu
from iti_system.schemas import MenuCreate, MenuUpdate 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"]) 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]) return ok([dump_menu(menu) for menu in menus])
@router.get("/tree", dependencies=[Depends(require_permission("system:menu:list"))]) @router.get("/tree")
def tree_menu(db: Session = Depends(get_db)): def tree_menu(user=Depends(current_user), db: Session = Depends(get_db)):
menus = db.scalars(select(SysMenu).order_by(SysMenu.sort, SysMenu.name)).all() menus = db.scalars(
return ok(build_tree(menus, dump_menu)) 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"))]) @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"))]) @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) stmt = select(SysMenu)
if id:
stmt = stmt.where(SysMenu.id != id)
if name: if name:
stmt = stmt.where(SysMenu.name == name) stmt = stmt.where(SysMenu.name == name)
if auth_code: if path:
stmt = stmt.where(SysMenu.auth_code == auth_code) 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}) return ok({"exists": db.scalar(stmt) is not None})

@ -21,7 +21,7 @@ from iti.db import get_db
from iti.exceptions import BizError, Unauthorized from iti.exceptions import BizError, Unauthorized
from iti.responses import pagination from iti.responses import pagination
from iti_system.enums import StatusEnum from iti_system.enums import MenuTypeEnum, StatusEnum
from iti_system.models import ( from iti_system.models import (
Role, Role,
SysConfig, 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]: def dump_user(user: User) -> dict[str, Any]:
role_codes = user.role_codes
return { return {
"id": user.id, "id": user.id,
"username": user.username, "username": user.username,
@ -111,8 +112,10 @@ def dump_user(user: User) -> dict[str, Any]:
"gender": user.gender, "gender": user.gender,
"status": user.status, "status": user.status,
"roles": [role.id for role in user.roles], "roles": [role.id for role in user.roles],
"roleCodes": role_codes,
"depts": user.dept_ids, "depts": user.dept_ids,
"permissions": user.permissions, "permissions": user.permissions,
"isSuper": "ADMIN" in role_codes,
"attributes": user.attribute_map, "attributes": user.attribute_map,
"createdAt": user.created_at, "createdAt": user.created_at,
"updatedAt": user.updated_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]: def dump_dept(dept: SysDept, *, with_children: bool = False) -> dict[str, Any]:
data = { data = {
"id": dept.id, "id": dept.id,

@ -1,9 +1,11 @@
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from iti import create_app from iti import create_app
from iti.auth import create_access_token
from iti.config import BaseConfig from iti.config import BaseConfig
from iti.db import Base from iti.db import Base
from iti_system import create_system_module from iti_system import create_system_module
from iti_system.models import Role, SysMenu, User
from iti_system.seeds import seed_system_data 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) response = client.get("/sys/user/current", headers=headers)
assert response.status_code == 200 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() event = app.state.audit_dispatcher._queue.get_nowait()
assert event.type == "login" assert event.type == "login"
assert event.actor_id == response.json()["data"]["id"] assert event.actor_id == data["id"]
assert event.success is True 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 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(): def test_internal_audit_writes_sys_log():
client = TestClient(make_app()) client = TestClient(make_app())

Loading…
Cancel
Save