`remark_changed` 此前是「定义了事件类型、但压根没有改备注的接口」,于是永远不发。
按 S4 的原则(留着一个永远不发的类型比删掉更糟 —— 看代码的人会以为它有数据),
补接口而不是删类型。
## 接口
`PUT /api/accounts/{id}/remark`,权限点 `account:remark`(授予 super_admin /
operation / support,与 `account:tag` 一致)。
- 与改标签一样走 `record_account_action()`:**审计 + 账号时间线一次写完**,
审计带 `account_id`,事件 detail 带 `{from, to}`;
- **只做单个、不做批量**:备注是自由文本,给一堆账号写同一句备注没有实际意义
(标签是短枚举才有批量);
- 内容 trim 后截断到 256(与 `accounts.remark` 列宽一致),清空也记一条事件;
- 备注没变化时直接返回、**不写事件**:否则时间线会被「点开保存了一下但没改」刷满。
## 权限点新增前的安全检查
新权限点会被 `custom_permissions` 覆盖,所以先查了库:
`admin` / `kf1_test` 的 `custom_permissions` 是 **JSON null**(不是 SQL NULL),
Python 侧取出来是 `None` → `get_user_permissions` 走角色默认。
也就是说**没有任何用户在用自定义权限列表**,新增权限点不会把谁的能力锁掉。
(这一点很容易看错:`IS NULL` 判出来是「有自定义权限」,`JSON_LENGTH` 还是 1。)
## 前端
- 详情页「备注」一行改为可编辑(铅笔 → 输入框 → 保存/取消,回车即存);
- `EVENT_STYLE` 补 `remark_changed`,时间线里能看到备注的每次变更。
## 一个自己踩到又修掉的坑
第一版把两个 `useState` 写在了 `if (!accountId) return <Empty/>` **之后** ——
正是本次会话刚在 LoginTasksPage 修掉的那类 hooks 顺序违规(`react-hooks/rules-of-hooks`
直接报错)。已上移到早期 return 之前的 state 区块。lint 抓到,构建不报,属于
「能跑但迟早炸」的那类问题。
## 验证
- `TZ=Asia/Shanghai`:**426 passed / 6 skipped**(S4 后 420 → 新增 6 条)
- `TZ=UTC`(相关文件)77 passed;ruff check/format 全绿
- 前端 `tsc -b && vite build` 通过、`npm run lint` 0 errors
- 新增用例:写入并带 from/to 事件与审计关联、未变化不写事件、清空也记一条、
trim + 截断到 256、账号不存在 404、**新权限点确实在三个角色的默认权限里**
(防止「加了接口但没人能用」)
153 lines
4.9 KiB
Python
153 lines
4.9 KiB
Python
"""权限点定义与角色默认权限"""
|
|
|
|
# ---- 权限点 ----
|
|
PERMISSIONS = {
|
|
# 用户管理
|
|
"user:view": "查看用户列表",
|
|
"user:create": "创建用户",
|
|
"user:edit": "编辑用户",
|
|
"user:rename": "修改用户名",
|
|
"user:delete": "删除用户",
|
|
"user:assign_permissions": "分配权限",
|
|
# 账号管理
|
|
"account:view_all": "查看所有账号(仅用户名、标签、分配)",
|
|
"account:view_full": "查看账号完整字段(密码/邮箱,仅管理员)",
|
|
"account:view_assigned": "查看分配给自己的账号",
|
|
"account:import": "导入账号",
|
|
"account:tag": "修改账号标签",
|
|
"account:remark": "修改账号备注",
|
|
"account:check": "账号检测",
|
|
"account:assign": "分配账号给客服",
|
|
"account:delete": "删除账号",
|
|
"account:manage": "停用/归档账号、开关自动化",
|
|
# 登录任务
|
|
"login:batch": "发起批量登录",
|
|
"login:view_all": "查看所有登录任务",
|
|
"login:view_assigned": "查看自己账号的登录任务",
|
|
# Cookie
|
|
"cookie:view": "查看 Cookie",
|
|
"cookie:operate": "检测/重登已分配账号 Cookie(不含查看)",
|
|
"cookie:export": "导出 Cookie",
|
|
# 斗鱼活动
|
|
"douyu:task": "斗鱼任务管理",
|
|
"douyu:config": "斗鱼配置管理",
|
|
"yyb:session": "应用宝扫码登录与角色查询",
|
|
"yyb:recharge": "应用宝充值与付款",
|
|
"yyb:manage": "接管并操作他人的应用宝任务",
|
|
"yyb:history": "查看应用宝充值历史",
|
|
# 虎牙
|
|
"huya:account": "虎牙账号管理(兼容旧权限)",
|
|
"huya:view_all": "查看所有虎牙账号",
|
|
"huya:view_assigned": "查看分配给自己的虎牙账号",
|
|
"huya:import": "导入/登录虎牙账号",
|
|
"huya:tag": "修改虎牙账号标签",
|
|
"huya:assign": "分配虎牙账号",
|
|
"huya:delete": "删除虎牙账号",
|
|
"huya:cookie:view": "查看虎牙 Cookie",
|
|
"huya:cookie:export": "导出/清除虎牙 Cookie",
|
|
"huya:task": "虎牙任务管理",
|
|
"huya:bind": "虎牙绑定操作",
|
|
"huya:recharge": "虎牙充值操作",
|
|
"huya:config": "虎牙配置管理",
|
|
# 代理 & 白名单
|
|
"proxy:manage": "代理配置管理",
|
|
"whitelist:manage": "白名单配置管理",
|
|
"whitelist:test": "测试白名单",
|
|
# 自动化
|
|
"automation:view": "查看自动化作业与运行历史",
|
|
"automation:manage": "修改自动化配置、手动触发作业",
|
|
# 系统
|
|
"system:settings": "系统设置",
|
|
"audit:view": "查看审计日志",
|
|
}
|
|
|
|
# ---- 角色默认权限 ----
|
|
ROLE_PERMISSIONS = {
|
|
"super_admin": list(PERMISSIONS.keys()), # 全部权限
|
|
"operation": [
|
|
"account:view_all",
|
|
"account:import",
|
|
"account:tag",
|
|
"account:remark",
|
|
"account:check",
|
|
"account:assign",
|
|
"account:manage",
|
|
"login:batch",
|
|
"login:view_all",
|
|
"cookie:view",
|
|
"cookie:export",
|
|
"douyu:task",
|
|
"douyu:config",
|
|
"yyb:session",
|
|
"yyb:recharge",
|
|
"yyb:manage",
|
|
"yyb:history",
|
|
"huya:account",
|
|
"huya:view_all",
|
|
"huya:import",
|
|
"huya:tag",
|
|
"huya:assign",
|
|
"huya:delete",
|
|
"huya:cookie:view",
|
|
"huya:cookie:export",
|
|
"huya:task",
|
|
"huya:bind",
|
|
"huya:recharge",
|
|
"huya:config",
|
|
"proxy:manage",
|
|
"whitelist:manage",
|
|
"whitelist:test",
|
|
"automation:view",
|
|
],
|
|
"support": [
|
|
"account:view_assigned",
|
|
"account:tag",
|
|
"account:remark",
|
|
"cookie:operate",
|
|
"douyu:task",
|
|
"yyb:session",
|
|
"yyb:history",
|
|
"huya:view_assigned",
|
|
"huya:tag",
|
|
"huya:task",
|
|
],
|
|
}
|
|
|
|
# 角色中文映射
|
|
ROLE_LABELS = {
|
|
"super_admin": "超级管理员",
|
|
"operation": "运营",
|
|
"support": "客服",
|
|
}
|
|
|
|
|
|
def get_role_permissions(role: str) -> list[str]:
|
|
"""获取角色拥有的权限列表。"""
|
|
return ROLE_PERMISSIONS.get(role, [])
|
|
|
|
|
|
def get_user_permissions(user) -> list[str]:
|
|
"""获取用户的实际权限列表。
|
|
优先使用用户自定义权限(custom_permissions),
|
|
若为 None 则回退到角色默认权限。
|
|
"""
|
|
if user.custom_permissions is not None:
|
|
return user.custom_permissions
|
|
return get_role_permissions(user.role)
|
|
|
|
|
|
def has_permission(role: str, permission: str) -> bool:
|
|
"""检查角色是否拥有某权限(仅检查角色默认权限,不考虑自定义权限)。
|
|
|
|
注意:对于需要考虑用户自定义权限的场景,请使用 user_has_permission()。
|
|
"""
|
|
return permission in get_role_permissions(role)
|
|
|
|
|
|
def user_has_permission(user, permission: str) -> bool:
|
|
"""检查用户是否拥有某权限(考虑自定义权限)。
|
|
|
|
优先使用用户的 custom_permissions,若为 None 则回退到角色默认权限。
|
|
"""
|
|
return permission in get_user_permissions(user)
|