修复主管点击成员管理没有反应的BUG
This commit is contained in:
parent
00239b0431
commit
7f851593fb
@ -249,7 +249,7 @@ DINGTALK_STATES: dict[str, float] = {}
|
|||||||
ROLE_LABELS = {"super_admin": "超级管理员", "dept_manager": "部门主管", "operator": "普通运营"}
|
ROLE_LABELS = {"super_admin": "超级管理员", "dept_manager": "部门主管", "operator": "普通运营"}
|
||||||
ROLE_PERMISSIONS = {
|
ROLE_PERMISSIONS = {
|
||||||
"super_admin": ["assistant", "library", "create", "board", "review", "refund-records", "usage", "users", "logs", "settings", "refund-management"],
|
"super_admin": ["assistant", "library", "create", "board", "review", "refund-records", "usage", "users", "logs", "settings", "refund-management"],
|
||||||
"dept_manager": ["assistant", "library", "create", "board", "review", "refund-records"],
|
"dept_manager": ["assistant", "library", "create", "board", "review", "refund-records", "users"],
|
||||||
"operator": ["assistant", "library", "create", "board", "review", "refund-records"],
|
"operator": ["assistant", "library", "create", "board", "review", "refund-records"],
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -565,6 +565,12 @@ def require_super_admin(user: AdminUser = Depends(current_user)) -> AdminUser:
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def require_user_manager(user: AdminUser = Depends(current_user)) -> AdminUser:
|
||||||
|
if user.role_code not in {"super_admin", "dept_manager"}:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="仅部门主管或超级管理员可管理成员")
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/health")
|
@app.get("/api/health")
|
||||||
def health():
|
def health():
|
||||||
return {"success": True, "message": "ok"}
|
return {"success": True, "message": "ok"}
|
||||||
@ -5186,6 +5192,27 @@ def _authorization_department_scope(manager: AdminUser):
|
|||||||
AdminUser.dingtalk_dept_path.contains(f",{manager.dingtalk_dept_id},", autoescape=True))
|
AdminUser.dingtalk_dept_path.contains(f",{manager.dingtalk_dept_id},", autoescape=True))
|
||||||
|
|
||||||
|
|
||||||
|
def _department_is_in_manager_scope(manager: AdminUser, dept_id: str | int | None, dept_path: str | None) -> bool:
|
||||||
|
manager_dept_id = str(manager.dingtalk_dept_id or "").strip()
|
||||||
|
if not manager_dept_id:
|
||||||
|
return False
|
||||||
|
return str(dept_id or "").strip() == manager_dept_id or f",{manager_dept_id}," in str(dept_path or "")
|
||||||
|
|
||||||
|
|
||||||
|
def _managed_user(db: Session, current: AdminUser, user_id: int, *, for_update: bool = False) -> AdminUser:
|
||||||
|
statement = select(AdminUser).where(AdminUser.id == user_id)
|
||||||
|
if current.role_code == "dept_manager":
|
||||||
|
statement = statement.where(AdminUser.role_code == "operator", _authorization_department_scope(current))
|
||||||
|
if for_update:
|
||||||
|
statement = statement.with_for_update()
|
||||||
|
user = db.scalar(statement)
|
||||||
|
if user:
|
||||||
|
return user
|
||||||
|
if current.role_code == "dept_manager" and db.get(AdminUser, user_id):
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="只能管理本部门及下级部门的普通运营")
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
||||||
|
|
||||||
|
|
||||||
@app.patch("/api/admin/users/{user_id}/payment-account")
|
@app.patch("/api/admin/users/{user_id}/payment-account")
|
||||||
def authorize_payment_account(user_id: int, payload: PaymentAccountAuthorization,
|
def authorize_payment_account(user_id: int, payload: PaymentAccountAuthorization,
|
||||||
current: AdminUser = Depends(current_user), db: Session = Depends(get_db)):
|
current: AdminUser = Depends(current_user), db: Session = Depends(get_db)):
|
||||||
@ -5207,18 +5234,26 @@ def authorize_payment_account(user_id: int, payload: PaymentAccountAuthorization
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/admin/users/{user_id}")
|
@app.get("/api/admin/users/{user_id}")
|
||||||
def user_detail(user_id: int, _: AdminUser = Depends(require_super_admin), db: Session = Depends(get_db)):
|
def user_detail(user_id: int, current: AdminUser = Depends(require_user_manager), db: Session = Depends(get_db)):
|
||||||
user = db.get(AdminUser, user_id)
|
user = _managed_user(db, current, user_id)
|
||||||
if not user:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
|
||||||
return {"success": True, "data": public_user(user) | {"is_active": user.is_active, "last_login_at": user.last_login_at.isoformat() if user.last_login_at else None}}
|
return {"success": True, "data": public_user(user) | {"is_active": user.is_active, "last_login_at": user.last_login_at.isoformat() if user.last_login_at else None}}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/admin/users", status_code=status.HTTP_201_CREATED)
|
@app.post("/api/admin/users", status_code=status.HTTP_201_CREATED)
|
||||||
def create_user(payload: AdminUserCreate, _: AdminUser = Depends(require_super_admin), db: Session = Depends(get_db)):
|
def create_user(payload: AdminUserCreate, current: AdminUser = Depends(require_user_manager), db: Session = Depends(get_db)):
|
||||||
if db.scalar(select(AdminUser).where(AdminUser.username == payload.username)):
|
if db.scalar(select(AdminUser).where(AdminUser.username == payload.username)):
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="用户名已存在")
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="用户名已存在")
|
||||||
user = AdminUser(username=payload.username, password_hash=hash_password(payload.password), display_name=payload.display_name, role_code=payload.role_code)
|
if current.role_code == "dept_manager" and payload.role_code != "operator":
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="部门主管只能新增普通运营")
|
||||||
|
user = AdminUser(
|
||||||
|
username=payload.username,
|
||||||
|
password_hash=hash_password(payload.password),
|
||||||
|
display_name=payload.display_name,
|
||||||
|
role_code=payload.role_code,
|
||||||
|
dingtalk_dept_id=current.dingtalk_dept_id if current.role_code == "dept_manager" else None,
|
||||||
|
dingtalk_dept_name=current.dingtalk_dept_name if current.role_code == "dept_manager" else None,
|
||||||
|
dingtalk_dept_path=current.dingtalk_dept_path if current.role_code == "dept_manager" else None,
|
||||||
|
)
|
||||||
db.add(user)
|
db.add(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
@ -5226,15 +5261,23 @@ def create_user(payload: AdminUserCreate, _: AdminUser = Depends(require_super_a
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/api/admin/users/dingtalk/lookup")
|
@app.post("/api/admin/users/dingtalk/lookup")
|
||||||
def lookup_dingtalk_user(payload: AdminDingTalkUserLookup, _: AdminUser = Depends(require_super_admin), db: Session = Depends(get_db)):
|
def lookup_dingtalk_user(payload: AdminDingTalkUserLookup, current: AdminUser = Depends(require_user_manager), db: Session = Depends(get_db)):
|
||||||
profile = _dingtalk_lookup_user(payload.identifier, db)
|
profile = _dingtalk_lookup_user(payload.identifier, db)
|
||||||
|
if current.role_code == "dept_manager" and not _department_is_in_manager_scope(
|
||||||
|
current, profile.get("_primary_dept_id"), profile.get("_dept_path")
|
||||||
|
):
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="只能添加本部门及下级部门的钉钉员工")
|
||||||
return {"success": True, "data": _dingtalk_user_preview(profile)}
|
return {"success": True, "data": _dingtalk_user_preview(profile)}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/admin/users/dingtalk", status_code=status.HTTP_201_CREATED)
|
@app.post("/api/admin/users/dingtalk", status_code=status.HTTP_201_CREATED)
|
||||||
def create_dingtalk_user(payload: AdminDingTalkUserCreate, _: AdminUser = Depends(require_super_admin), db: Session = Depends(get_db)):
|
def create_dingtalk_user(payload: AdminDingTalkUserCreate, current: AdminUser = Depends(require_user_manager), db: Session = Depends(get_db)):
|
||||||
profile = _dingtalk_lookup_user(payload.identifier, db)
|
profile = _dingtalk_lookup_user(payload.identifier, db)
|
||||||
preview = _dingtalk_user_preview(profile)
|
preview = _dingtalk_user_preview(profile)
|
||||||
|
if current.role_code == "dept_manager" and not _department_is_in_manager_scope(
|
||||||
|
current, profile.get("_primary_dept_id"), profile.get("_dept_path")
|
||||||
|
):
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="只能添加本部门及下级部门的钉钉员工")
|
||||||
user_id = preview["user_id"]
|
user_id = preview["user_id"]
|
||||||
union_id = preview["union_id"]
|
union_id = preview["union_id"]
|
||||||
if not user_id and not union_id:
|
if not user_id and not union_id:
|
||||||
@ -5248,6 +5291,12 @@ def create_dingtalk_user(payload: AdminDingTalkUserCreate, _: AdminUser = Depend
|
|||||||
conditions.append(AdminUser.username == username)
|
conditions.append(AdminUser.username == username)
|
||||||
user = db.scalar(select(AdminUser).where(or_(*conditions)))
|
user = db.scalar(select(AdminUser).where(or_(*conditions)))
|
||||||
role_code = payload.role_code or preview["suggested_role"]
|
role_code = payload.role_code or preview["suggested_role"]
|
||||||
|
if current.role_code == "dept_manager":
|
||||||
|
if user and user.role_code != "operator":
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="部门主管不能修改主管或超级管理员账号")
|
||||||
|
if role_code != "operator":
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="部门主管只能新增普通运营")
|
||||||
|
role_code = "operator"
|
||||||
if user and user.dingtalk_user_id and user_id and user.dingtalk_user_id != user_id:
|
if user and user.dingtalk_user_id and user_id and user.dingtalk_user_id != user_id:
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="该钉钉身份已绑定其他后台账号")
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="该钉钉身份已绑定其他后台账号")
|
||||||
if user and user.dingtalk_union_id and union_id and user.dingtalk_union_id != union_id:
|
if user and user.dingtalk_union_id and union_id and user.dingtalk_union_id != union_id:
|
||||||
@ -5286,11 +5335,11 @@ def create_dingtalk_user(payload: AdminDingTalkUserCreate, _: AdminUser = Depend
|
|||||||
|
|
||||||
|
|
||||||
@app.patch("/api/admin/users/{user_id}")
|
@app.patch("/api/admin/users/{user_id}")
|
||||||
def update_user(user_id: int, payload: AdminUserUpdate, current: AdminUser = Depends(require_super_admin), db: Session = Depends(get_db)):
|
def update_user(user_id: int, payload: AdminUserUpdate, current: AdminUser = Depends(require_user_manager), db: Session = Depends(get_db)):
|
||||||
user = db.get(AdminUser, user_id)
|
user = _managed_user(db, current, user_id, for_update=True)
|
||||||
if not user:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
|
||||||
changes = payload.model_dump(exclude_unset=True)
|
changes = payload.model_dump(exclude_unset=True)
|
||||||
|
if current.role_code == "dept_manager" and changes.get("role_code") not in (None, "operator"):
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="部门主管不能提升成员角色")
|
||||||
if user.id == current.id and (changes.get("is_active") is False or changes.get("role_code") not in (None, "super_admin")):
|
if user.id == current.id and (changes.get("is_active") is False or changes.get("role_code") not in (None, "super_admin")):
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不能停用或降级当前登录账号")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不能停用或降级当前登录账号")
|
||||||
if changes.get("role_code") in {"operator", "dept_manager"} and user.role_code == "super_admin":
|
if changes.get("role_code") in {"operator", "dept_manager"} and user.role_code == "super_admin":
|
||||||
@ -5307,10 +5356,8 @@ def update_user(user_id: int, payload: AdminUserUpdate, current: AdminUser = Dep
|
|||||||
|
|
||||||
|
|
||||||
@app.delete("/api/admin/users/{user_id}")
|
@app.delete("/api/admin/users/{user_id}")
|
||||||
def delete_user(user_id: int, current: AdminUser = Depends(require_super_admin), db: Session = Depends(get_db)):
|
def delete_user(user_id: int, current: AdminUser = Depends(require_user_manager), db: Session = Depends(get_db)):
|
||||||
user = db.get(AdminUser, user_id)
|
user = _managed_user(db, current, user_id, for_update=True)
|
||||||
if not user:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
|
||||||
if user.id == current.id:
|
if user.id == current.id:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不能删除当前登录账号")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不能删除当前登录账号")
|
||||||
if user.role_code == "super_admin":
|
if user.role_code == "super_admin":
|
||||||
|
|||||||
@ -30,8 +30,22 @@ def test_manager_department_scope_and_revocation(setup):
|
|||||||
db.commit()
|
db.commit()
|
||||||
account_id = owner.refund_payment_account_id
|
account_id = owner.refund_payment_account_id
|
||||||
main.app.dependency_overrides[main.current_user] = lambda: manager
|
main.app.dependency_overrides[main.current_user] = lambda: manager
|
||||||
|
assert "users" in main.public_user(manager)["permissions"]
|
||||||
rows = client.get("/api/admin/users").json()["data"]
|
rows = client.get("/api/admin/users").json()["data"]
|
||||||
assert {row["id"] for row in rows} == {owner.id, child.id}
|
assert {row["id"] for row in rows} == {owner.id, child.id}
|
||||||
|
assert client.get(f"/api/admin/users/{owner.id}").status_code == 200
|
||||||
|
assert client.get(f"/api/admin/users/{outsider.id}").status_code == 403
|
||||||
|
assert client.patch(f"/api/admin/users/{owner.id}", json={"display_name": "本部门成员"}).status_code == 200
|
||||||
|
assert client.patch(f"/api/admin/users/{owner.id}", json={"role_code": "dept_manager"}).status_code == 403
|
||||||
|
created = client.post("/api/admin/users", json={
|
||||||
|
"username": "manager-created", "password": "password123", "display_name": "主管新增", "role_code": "operator",
|
||||||
|
})
|
||||||
|
assert created.status_code == 201
|
||||||
|
created_id = created.json()["data"]["user"]["id"]
|
||||||
|
created_user = db.get(AdminUser, created_id)
|
||||||
|
assert str(created_user.dingtalk_dept_id) == str(manager.dingtalk_dept_id)
|
||||||
|
assert client.delete(f"/api/admin/users/{created_id}").status_code == 200
|
||||||
|
assert client.delete(f"/api/admin/users/{outsider.id}").status_code == 403
|
||||||
for target in (owner, child):
|
for target in (owner, child):
|
||||||
assert client.patch(f"/api/admin/users/{target.id}/payment-account", json={"account_id": account_id}).status_code == 200
|
assert client.patch(f"/api/admin/users/{target.id}/payment-account", json={"account_id": account_id}).status_code == 200
|
||||||
for target in (outsider, manager):
|
for target in (outsider, manager):
|
||||||
|
|||||||
@ -2343,6 +2343,7 @@ function AssistantPageFinal() {
|
|||||||
function UserTable({ users = [], onChanged, currentUser }) {
|
function UserTable({ users = [], onChanged, currentUser }) {
|
||||||
const rows = Array.isArray(users) ? users : [];
|
const rows = Array.isArray(users) ? users : [];
|
||||||
const isSuperAdmin = currentUser?.role_code === "super_admin";
|
const isSuperAdmin = currentUser?.role_code === "super_admin";
|
||||||
|
const canManageMembers = ["super_admin", "dept_manager"].includes(currentUser?.role_code);
|
||||||
const [authorization, setAuthorization] = useState(null);
|
const [authorization, setAuthorization] = useState(null);
|
||||||
const [accounts, setAccounts] = useState([]);
|
const [accounts, setAccounts] = useState([]);
|
||||||
const [accountId, setAccountId] = useState("");
|
const [accountId, setAccountId] = useState("");
|
||||||
@ -2493,10 +2494,10 @@ function UserTable({ users = [], onChanged, currentUser }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="user-table-actions">
|
<div className="user-table-actions">
|
||||||
{isSuperAdmin ? <button className="primary-button" onClick={() => open(null)}>
|
{canManageMembers ? <button className="primary-button" onClick={() => open(null)}>
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
{"\u65b0\u589e\u7528\u6237"}
|
{"\u65b0\u589e\u7528\u6237"}
|
||||||
</button> : <span>可为本部门及下级部门的普通运营设置付款账户授权。</span>}
|
</button> : null}
|
||||||
</div>
|
</div>
|
||||||
{authorization && (
|
{authorization && (
|
||||||
<div className="user-modal-backdrop">
|
<div className="user-modal-backdrop">
|
||||||
@ -2629,6 +2630,7 @@ function UserTable({ users = [], onChanged, currentUser }) {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
{isSuperAdmin ? (
|
||||||
<label>
|
<label>
|
||||||
{"\u89d2\u8272"}
|
{"\u89d2\u8272"}
|
||||||
<select
|
<select
|
||||||
@ -2639,11 +2641,10 @@ function UserTable({ users = [], onChanged, currentUser }) {
|
|||||||
>
|
>
|
||||||
<option value="operator">{"\u666e\u901a\u8fd0\u8425"}</option>
|
<option value="operator">{"\u666e\u901a\u8fd0\u8425"}</option>
|
||||||
<option value="dept_manager">部门主管</option>
|
<option value="dept_manager">部门主管</option>
|
||||||
<option value="super_admin">
|
<option value="super_admin">{"\u8d85\u7ea7\u7ba1\u7406\u5458"}</option>
|
||||||
{"\u8d85\u7ea7\u7ba1\u7406\u5458"}
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
) : <input type="hidden" value="operator" />}
|
||||||
{editing !== "new" && (
|
{editing !== "new" && (
|
||||||
<label className="checkbox-row">
|
<label className="checkbox-row">
|
||||||
<input
|
<input
|
||||||
@ -2708,7 +2709,7 @@ function UserTable({ users = [], onChanged, currentUser }) {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{user.role_code === "operator" && <button className="table-action" onClick={() => openAuthorization(user)}>付款账户授权</button>}
|
{user.role_code === "operator" && <button className="table-action" onClick={() => openAuthorization(user)}>付款账户授权</button>}
|
||||||
{isSuperAdmin && <><button className="table-action" onClick={() => open(user)}>
|
{canManageMembers && <><button className="table-action" onClick={() => open(user)}>
|
||||||
{"\u7f16\u8f91"}
|
{"\u7f16\u8f91"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user