From baed0e61ff64e77280d27c45d7c381627add93d4 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 14 Aug 2026 12:44:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AE=A2=E6=9C=8D=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E6=A0=87=E7=AD=BE=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_account_sensitive_fields.py | 28 +++++++++- web/backend/permissions.py | 6 +++ web/backend/routers/accounts.py | 26 +++++++--- web/backend/routers/huya.py | 18 ++++--- web/frontend/src/pages/AccountsPage.tsx | 57 +++++++++------------ web/frontend/src/pages/HuyaAccountsPage.tsx | 57 +++++++++------------ 6 files changed, 111 insertions(+), 81 deletions(-) diff --git a/tests/test_account_sensitive_fields.py b/tests/test_account_sensitive_fields.py index 0520c66..327408c 100644 --- a/tests/test_account_sensitive_fields.py +++ b/tests/test_account_sensitive_fields.py @@ -6,10 +6,12 @@ os.environ.setdefault("APP_ENCRYPTION_KEY", "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODl from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +from fastapi import HTTPException from web.backend.database import Base from web.backend.models import Account, User -from web.backend.routers.accounts import list_accounts +from web.backend.routers.accounts import list_accounts, set_account_tag +from web.backend.schemas import AccountTag class AccountSensitiveFieldsTests(unittest.TestCase): @@ -68,6 +70,30 @@ class AccountSensitiveFieldsTests(unittest.TestCase): self.assertEqual(item.email, "account@example.com") self.assertEqual(item.email_password, "email-password") + def test_support_can_only_change_tags_on_assigned_accounts(self): + support = User(username="support", password_hash="hash", role="support") + other_support = User(username="other-support", password_hash="hash", role="support") + self.session.add_all([support, other_support]) + self.session.commit() + assigned = Account( + username="assigned", password="password", email="assigned@example.com", + email_password="mail-password", assigned_to=support.id, + ) + other = Account( + username="other", password="password", email="other@example.com", + email_password="mail-password", assigned_to=other_support.id, + ) + self.session.add_all([assigned, other]) + self.session.commit() + + set_account_tag(assigned.id, AccountTag(tag="客服组"), db=self.session, current=support) + self.session.refresh(assigned) + self.assertEqual(assigned.tag, "客服组") + + with self.assertRaises(HTTPException) as context: + set_account_tag(other.id, AccountTag(tag="越权"), db=self.session, current=support) + self.assertEqual(context.exception.status_code, 404) + if __name__ == "__main__": unittest.main() diff --git a/web/backend/permissions.py b/web/backend/permissions.py index 322afbc..6882c18 100644 --- a/web/backend/permissions.py +++ b/web/backend/permissions.py @@ -14,6 +14,7 @@ PERMISSIONS = { "account:view_full": "查看账号完整字段(密码/邮箱,仅管理员)", "account:view_assigned": "查看分配给自己的账号", "account:import": "导入账号", + "account:tag": "修改账号标签", "account:check": "账号检测", "account:assign": "分配账号给客服", "account:delete": "删除账号", @@ -37,6 +38,7 @@ PERMISSIONS = { "huya:view_all": "查看所有虎牙账号", "huya:view_assigned": "查看分配给自己的虎牙账号", "huya:import": "导入/登录虎牙账号", + "huya:tag": "修改虎牙账号标签", "huya:assign": "分配虎牙账号", "huya:delete": "删除虎牙账号", "huya:cookie:view": "查看虎牙 Cookie", @@ -60,6 +62,7 @@ ROLE_PERMISSIONS = { "operation": [ "account:view_all", "account:import", + "account:tag", "account:check", "account:assign", "login:batch", @@ -75,6 +78,7 @@ ROLE_PERMISSIONS = { "huya:account", "huya:view_all", "huya:import", + "huya:tag", "huya:assign", "huya:delete", "huya:cookie:view", @@ -89,11 +93,13 @@ ROLE_PERMISSIONS = { ], "support": [ "account:view_assigned", + "account:tag", "cookie:operate", "douyu:task", "yyb:session", "yyb:history", "huya:view_assigned", + "huya:tag", "huya:task", ], } diff --git a/web/backend/routers/accounts.py b/web/backend/routers/accounts.py index 2bed7d2..d6a44a6 100644 --- a/web/backend/routers/accounts.py +++ b/web/backend/routers/accounts.py @@ -92,6 +92,15 @@ def _selected_account_ids(db: Session, current: User, req: AccountBulkSelection) return [account_id for account_id in ids if account_id in allowed] +def _require_account_tag_permission(current: User) -> None: + """标签权限兼容历史上使用账号导入权限的运营人员。""" + if not ( + user_has_permission(current, "account:tag") + or user_has_permission(current, "account:import") + ): + raise HTTPException(status_code=403, detail="无权修改账号标签") + + @router.get("") def list_accounts( assigned_only: bool = Query(False), @@ -323,13 +332,16 @@ def set_account_tag( account_id: int, req: AccountTag, db: Session = Depends(get_db), - current: User = Depends(require_permission("account:import")), + current: User = Depends(get_current_user), ): - """设置单个账号标签。""" - acc = db.query(Account).filter(Account.id == account_id).first() + """设置单个可见账号的标签。""" + _require_account_tag_permission(current) + acc = _visible_accounts_query(db, current).filter(Account.id == account_id).first() if not acc: raise HTTPException(status_code=404, detail="账号不存在") acc.tag = (req.tag or "").strip() + db.add(AuditLog(user_id=current.id, username=current.username, + action="account:tag", target=acc.username, detail=acc.tag)) db.commit() return {"message": "标签已更新", "success": True} @@ -338,13 +350,14 @@ def set_account_tag( def batch_tag( req: AccountTag, db: Session = Depends(get_db), - current: User = Depends(require_permission("account:import")), + current: User = Depends(get_current_user), ): """批量设置账号标签。""" + _require_account_tag_permission(current) if not req.account_ids: raise HTTPException(status_code=400, detail="请选择账号") tag = (req.tag or "").strip() - count = db.query(Account).filter(Account.id.in_(req.account_ids)).update( + count = _visible_accounts_query(db, current).filter(Account.id.in_(req.account_ids)).update( {Account.tag: tag}, synchronize_session=False ) db.commit() @@ -355,9 +368,10 @@ def batch_tag( def batch_tag_selection( req: AccountBulkTag, db: Session = Depends(get_db), - current: User = Depends(require_permission("account:import")), + current: User = Depends(get_current_user), ): """按显式选择或当前筛选结果批量设置账号标签。""" + _require_account_tag_permission(current) ids = _selected_account_ids(db, current, req) if not ids: raise HTTPException(status_code=400, detail="请选择账号") diff --git a/web/backend/routers/huya.py b/web/backend/routers/huya.py index 25014cc..02133b1 100644 --- a/web/backend/routers/huya.py +++ b/web/backend/routers/huya.py @@ -94,6 +94,12 @@ def _require_huya_perm(user: User, permission: str) -> None: raise HTTPException(status_code=403, detail="权限不足") +def _require_huya_tag_permission(user: User) -> None: + """标签权限兼容历史上使用导入权限的运营人员。""" + if not (_has_huya_perm(user, "huya:tag") or _has_huya_perm(user, "huya:import")): + raise HTTPException(status_code=403, detail="无权修改虎牙账号标签") + + def _can_view_huya_all(user: User) -> bool: return _has_huya_perm(user, "huya:view_all") @@ -1064,8 +1070,8 @@ def set_account_tag( current: User = Depends(get_current_user), ): """设置单个虎牙账号标签。""" - _require_huya_perm(current, "huya:import") - account = db.query(HuyaAccount).filter(HuyaAccount.id == account_id).first() + _require_huya_tag_permission(current) + account = _visible_huya_accounts_query(db, current).filter(HuyaAccount.id == account_id).first() if not account: raise HTTPException(status_code=404, detail="账号不存在") account.tag = (req.tag or "").strip() @@ -1080,11 +1086,11 @@ def batch_tag( current: User = Depends(get_current_user), ): """批量设置虎牙账号标签。""" - _require_huya_perm(current, "huya:import") + _require_huya_tag_permission(current) if not req.account_ids: raise HTTPException(status_code=400, detail="请选择账号") tag = (req.tag or "").strip() - count = db.query(HuyaAccount).filter(HuyaAccount.id.in_(req.account_ids)).update( + count = _visible_huya_accounts_query(db, current).filter(HuyaAccount.id.in_(req.account_ids)).update( {HuyaAccount.tag: tag}, synchronize_session=False, ) @@ -1099,12 +1105,12 @@ def batch_tag_selection( current: User = Depends(get_current_user), ): """按显式选择或当前筛选结果批量设置虎牙账号标签。""" - _require_huya_perm(current, "huya:import") + _require_huya_tag_permission(current) ids = _selected_huya_account_ids(db, current, req) if not ids: raise HTTPException(status_code=400, detail="请选择虎牙账号") tag = (req.tag_value or "").strip() - count = db.query(HuyaAccount).filter(HuyaAccount.id.in_(ids)).update( + count = _visible_huya_accounts_query(db, current).filter(HuyaAccount.id.in_(ids)).update( {HuyaAccount.tag: tag}, synchronize_session=False, ) diff --git a/web/frontend/src/pages/AccountsPage.tsx b/web/frontend/src/pages/AccountsPage.tsx index a6c9182..37a84fa 100644 --- a/web/frontend/src/pages/AccountsPage.tsx +++ b/web/frontend/src/pages/AccountsPage.tsx @@ -45,9 +45,10 @@ export default function AccountsPage() { const canViewFull = can('account:view_full'); const canImport = can('account:import'); + const canTag = can('account:tag') || canImport; const canAssign = can('account:assign'); const canDelete = can('account:delete'); - const canSelectRows = canImport || canDelete || canAssign; + const canSelectRows = canTag || canDelete || canAssign; const loadAccounts = useCallback(async () => { setLoading(true); @@ -252,43 +253,29 @@ export default function AccountsPage() { { title: '标签', dataIndex: 'tag', - width: 120, + width: 170, render: (tag: string, record: AccountItem) => { - if (!tag) { - if (canImport) { - return ( - { - const val = (e.target as HTMLInputElement).value.trim(); - if (val) handleSetTag(record.id, val); - }} - onBlur={(e) => { - const val = e.target.value.trim(); - if (val) handleSetTag(record.id, val); - }} - /> - ); - } - return -; - } - if (canImport) { + if (canTag) { return ( - { - e.preventDefault(); - handleSetTag(record.id, ''); + )} - {canImport && ( + {canTag && ( - {canImport && ( + {canTag && (