Skip to content

feat: 页面头部账户下拉展示当前用户信息 - #36

Merged
hm1229 merged 9 commits into
OpenBMB:mainfrom
tianling536:feat/user-info-card
Jul 29, 2026
Merged

feat: 页面头部账户下拉展示当前用户信息#36
hm1229 merged 9 commits into
OpenBMB:mainfrom
tianling536:feat/user-info-card

Conversation

@tianling536

Copy link
Copy Markdown
Contributor

概述

页面头部账户下拉此前只有「退出登录」一项,全站没有任何地方展示当前登录用户(官方最新代码同样缺失)。本 PR 在下拉顶部新增用户卡片。

实现

  • AppHeader.tsx 下拉顶部新增用户卡片:大头像(显示名首字母)+ 显示名 + @username + 角色徽章(管理员/成员)+ admin 可见租户 ID;分隔线下保留原「退出登录」;
  • 组件内直接读取 auth session(getEnterpriseAuthSession()),零页面调用点改动;userName prop 仅作头像兜底;
  • 降级链完整:无 display_name 回退 username(省略 @行)、无 session 时与现状完全一致、长文本 240px+truncate 防破版;
  • i18n 新增 1 条(成员 → Member)。

验证

  • npm run build 通过;
  • 浏览器实测:admin 账号正确渲染「A / Administrator / @admin / 管理员 / tenant_demo / 退出登录」;
  • npm run i18n:check 仅剩 main 基线存量缺失(WorkRecordTab 一条,与本 PR 无关,feat: 渠道可靠性与工程强化(遗留项 P0-P3) #33 已覆盖修复)。

单 commit,2 个文件(AppHeader.tsx、en.json),与渠道工作完全独立。

Comment thread frontend-enterprise/src/components/AppHeader.tsx Fixed
Comment thread frontend-enterprise/src/components/AppHeader.tsx Fixed
Comment thread frontend-enterprise/src/components/AppHeader.tsx Fixed
Comment thread frontend-enterprise/src/components/AppHeader.tsx Fixed
Comment thread frontend-enterprise/src/components/AppHeader.tsx Fixed
Comment thread frontend-enterprise/src/components/AppHeader.tsx Fixed
@tianling536

Copy link
Copy Markdown
Contributor Author

关于 CodeQL 的 3 条 js/xss 高危告警(AppHeader.tsx 170/199/287 行,<img src={...}>):经分析为误报,说明如下。

  • 三处的 src 值只有两个来源:URL.createObjectURL() 生成的 blob URL(上传预览),或后端按字节头嗅探构造的 data:image/* URL(已存头像)——均不可能为 javascript: 等可执行协议;
  • React 对属性值默认转义,不存在未转义 HTML 注入面;
  • 我们已进一步加防御:通过 new URL() 解析并仅放行 http/https/data:image/blob 协议(commit 5ff3a5b/66b0bf3),即使未来值被污染也不会被执行。

CodeQL 的污点模型不识别自定义协议校验函数,因此仍标记这 3 行。烦请有仓库写权限的同学在 code scanning 中将这 3 条标记为 false positive 处理;若贵方有惯用的通过模式(如统一的图片 URL 工具函数),也欢迎指出,我按此调整。

@hm1229
hm1229 self-requested a review July 27, 2026 02:30
@hm1229

hm1229 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

[P1] 点击解除一个身份,会删除该渠道下的全部身份绑定
前端传了 external_user_id:
...?tenant_id=...&external_user_id=...
但后端接口根本不接收这个参数,并循环删除当前用户在该渠道下的所有身份。

[P2] 渠道身份作用域前后端契约不完整
前端新增并读取 external_account_scope,但后端响应模型和列表接口都没有返回它。

[P2] 上传预览的 Blob URL 没有被正确释放
pickAvatar() 创建 objectUrl 后,finally 调用的 clearUploadPreview() 仍捕获上传开始前的旧 state,因此不会 revoke 新 URL。每次上传都会泄漏一个 Blob URL,直到页面卸载。

[P2] 2MB 限制是在完整读入内存后才检查
认证用户仍可以发送远大于 2MB 的文件,使服务完整读入内存。

@hm1229 hm1229 self-assigned this Jul 27, 2026
@tianling536

Copy link
Copy Markdown
Contributor Author

感谢复核,四条意见已全部处理(commit c209ebc):

[P1] 点击解除一个身份,会删除该渠道下的全部身份绑定
后端 DELETE /my-identity-bindings/{channel} 现接收 external_user_idexternal_account_scope 查询参数:传 external_user_id 时只解绑该外部身份;同时传 scope 时按完整身份键精确定位一行;未传 scope 但该 external_user_id 在多 scope(多企业)下均有绑定时返回 400 要求显式指定,不盲删;不传 external_user_id 保持原"按 channel 全部解绑"语义。前端解绑请求同步补上了 scope 参数。
回归测试:按 external_user_id 只解绑目标行(会话指针同步回懒建账号、另一行不动)、目标不存在 404 且不动现有绑定、多 scope 歧义 400 不盲删、带 scope 精确解绑 corpA 保留 corpB。

[P2] 渠道身份作用域前后端契约不完整
MyIdentityBindingRead 增加 external_account_scope 字段,列表接口一并返回(wechat 为空串),前端 scope 标签与行 key 读取的值现在与后端契约一致。已补列表返回 scope 的断言测试。

[P2] 上传预览的 Blob URL 没有被正确释放
预览 URL 改为 ref 跟踪(replaceUploadPreview):替换、清除(finally)与组件卸载时都 revoke 最新值,不再受 pickAvatar 闭包内旧 state 快照影响。

[P2] 2MB 限制是在完整读入内存后才检查
两道防线:先按 Content-Length 预检(请求体 >2MB+64KB multipart 开销直接 413),再限量读取(最多 MAX+1 字节)做硬性兜底,覆盖 Content-Length 缺失或虚报的情况。已补预检路径的 413 测试。

验证:后端全量 999 passed、ruff 通过;前端 build 与 i18n:check 通过。

田领 added 8 commits July 27, 2026 22:58
- AppHeader 下拉顶部新增用户卡片:大头像/显示名/@用户名/
  角色徽章(管理员/成员)/admin 可见租户
- 组件内读 auth session,零页面调用点改动;无 session 时与现状一致
- 全站此前无任何用户信息展示(官方最新代码亦缺失)
- 后端:user_avatars 独立小表(data_url 存库),
  PUT/DELETE /api/auth/me/avatar(字节头嗅探防伪装,≤2MB),
  login 与 auth/me 返回 avatar_url
- 前端:用户卡片更换头像(选图-预览-保存)与移除头像,
  上传后刷新会话,AppHeader 有图显图无图回首字母
- 浏览器实测上传全流程通过
打开文件对话框前清空 input.value,确保用户多次选择
同一图片时预览与保存流程始终可用
- 文件 input 移出下拉菜单常驻 header 根部,菜单关闭不再卸载 input
- 预览/保存改为独立 Dialog,下拉菜单因系统对话框失焦关闭也不影响流程
- 修复真实桌面环境(非 Playwright)选图后无任何反馈的问题
img src 动态值限制为 http(s)/data:image/blob 协议,
修复 3 处 js/xss 高危告警
- 选择图片后立即上传并乐观渲染,带上传中指示
- 失败自动回滚并提示;移除预览/保存独立弹窗
- AppHeader 预览 blob URL 改 ref 跟踪,替换/清除/组件卸载均正确 revoke,
  不受 pickAvatar 闭包旧 state 快照影响
- 头像上传先按 Content-Length 预检(>2MB+64KB 直接 413),再限量读取
  (最多 MAX+1 字节)硬兜底,超限不再完整读入内存
- 回归测试:Content-Length 预检路径 413
@tianling536
tianling536 force-pushed the feat/user-info-card branch from c209ebc to e1da611 Compare July 27, 2026 15:13
@tianling536

Copy link
Copy Markdown
Contributor Author

冲突已解决:分支已 rebase 到最新 main(cd64f92),当前 GitHub 显示 MERGEABLE。

说明一下本次 rebase 的结构变化:

  1. 身份绑定相关的两条复核修复(按行解绑、scope 契约)已被 main 吸收——渠道加固 PR 合并后,main 上的 delete_my_identity_binding 已支持 external_user_id/external_account_scope 按行解绑、MyIdentityBindingRead 已返回 scope、前端解绑请求也已携带 scope 参数。rebase 后本 PR 不再重复携带这部分改动,相应回归测试由 main 上的版本提供。
  2. 本 PR 保留的复核修复(commit e1da611):
    • [P2] blob URL 泄漏:预览 URL 改 ref 跟踪,替换/清除/组件卸载均正确 revoke,不受 pickAvatar 闭包旧 state 快照影响;
    • [P2] 2MB 读入后检查:先按 Content-Length 预检(>2MB+64KB 直接 413),再限量读取(最多 MAX+1 字节)硬兜底;已补预检路径测试。

验证:后端 1190 passed、ruff 通过;前端 build 通过。

另外报告两个 main 上已存在、与本 PR 无关 的问题(在纯净 main 上同样复现,未在本 PR 中修复以免扩大范围):

  • tests/test_model_configs_api.py::test_verification_runs_bounded_text_stream_and_json_probes 失败:期望 bounded probe max_tokens=32,实际 8192,疑似 capabilities provider 系列提交后探针上限未生效;
  • npm run i18n:check 退出码 1:main 新增的 channelPresentation.ts 有 12 条文案未入 en.json。
    如需我在本 PR 顺带修掉其中任何一项,请指示。

@hm1229

hm1229 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

[P1]
后端在 /login、/me 返回完整 Base64 头像,而前端把整个 user 写进 localStorage:
2MB 原图编码后约 2.67MB;部分浏览器或 localStorage 已有数据时可能触发 QuotaExceededError。而登录页会把这个错误误报成账号/密码错误,用户可能上传头像后无法重新登录。
建议不要持久化 avatar_url,由 Header 单独获取;更理想的是后端返回头像资源 URL,而不是在登录响应中内联二进制。至少也应显著降低头像限制并处理存储异常。

[P2] 删除用户不会删除头像记录
UserAvatar.user_id 没有外键级联,建议显式删除头像或增加级联,并补一条测试。

- login/me/上传响应的 avatar_url 一律为资源指针 /api/auth/me/avatar,
  不再内联 base64 二进制(原 2MB 图≈2.67MB JSON 会把 localStorage
  撑爆,QuotaExceededError 还被登录页误报为账号密码错误)
- 新增 GET /me/avatar 字节端点(content-type 嗅探+no-cache),
  前端凭指针用认证请求拉字节转 blob URL 渲染;blob 生命周期沿用
  ref 跟踪(替换/卸载均 revoke)
- setEnterpriseAuthSession 存储异常降级重试,最终抛真实原因,
  不再被误报为凭据错误
- delete_user 显式级联删除 UserAvatar,不留孤儿记录
- 测试:指针契约/字节端点(200/404/401)/覆盖写/级联删除共 12 例
@tianling536

Copy link
Copy Markdown
Contributor Author

感谢复核,两条意见已全部处理(commit df35e05):

[P1] 头像内联进 login/me + localStorage 撑爆、误报为凭据错误

按"更理想"方案实施,二进制不再出现在 login/me 响应和会话存储里:

  • 契约变更:login/me/上传响应中的 avatar_url 一律改为资源指针 /api/auth/me/avatar(仅作存在性标识,十几个字节)——登录响应和 localStorage 里的会话体积回到 KB 级,QuotaExceededError 根因消除;
  • 新增 GET /api/auth/me/avatar 资源端点:返回图片字节(content-type 按存储值嗅探,Cache-Control: private, no-cache),无头像 404、未认证 401;存储数据损坏按 404 处理并记录告警;
  • 前端 Header:不再从会话 user 取头像地址,改为凭指针用认证 fetch 拉字节 → blob URL 渲染(img 标签无法带 Bearer 头,这是不引入 query token/公开端点前提下最干净的加载方式);blob URL 沿用 ref 跟踪,替换/卸载均正确 revoke;覆盖上传后指针字符串不变,显式重拉字节保证不显示旧图;
  • 存储兜底:setEnterpriseAuthSession 写入失败时降级为最小会话重试,仍失败则抛出真实原因("浏览器存储空间不足...")——即使未来有别的大字段进会话,也不会再被误报为账号/密码错误。

[P2] 删除用户不删头像记录

delete_user 现在显式级联删除 UserAvatar 行(无外键级联,注释说明),并补了"管理员删除带头像用户后两行均不存在"的断言测试。

验证:后端全量 1193 passed(test_user_avatar 12 例覆盖指针契约/字节端点 200·404·401/覆盖写/级联删除/大小预检)、ruff 通过;前端 build 通过,新增文案已入 en.json。i18n:check 的 38 条未翻译与 test_verification_runs_bounded_text_stream_and_json_probes 失败均与纯净 main 完全一致(预存在问题,上轮已报告),本 PR 未引入新增。

@hm1229
hm1229 merged commit 3fc9653 into OpenBMB:main Jul 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants