跨端: fix(客户端) 换身份必须清全部账号数据 + 鸿蒙管理台门禁改三态
两份审查报告(`docs/reviews/electron-gui-review.md` /
`harmony-client-review.md`)里两条**数据隔离**缺陷。
## ① 换身份不清数据 ⇒ 在新账号的界面下显示旧账号的邮件
`setActive` 之后 `api/config` 单例里的 API_BASE 与 bearer 就翻到了新账号,
于是此后每个请求都带**新账号**的凭证。而各 store 里还留着**旧**账号的:
· mailStore.sent / currentMail
· sessionStore.sessions / currentSession / currentSessionMails
· contactStore.contacts / archivedContacts
⇒ 肉眼完全看不出来(不报错、不空屏),而**从旧视图发出的写操作**
(归档 / 转发 / 批准权限)改的是**新账号**。
新增 `src/lib/resetAccountData.ts` —— **一处实现,三个入口都调它**:
① 主动切账号(AccountSwitcher.pick)
② 登出(authStore.logout)
③ 任意接口 401(api/client.ts 的 unauthorized 回调 → markAnonymous)
只在 ① 里清是最容易漏的那种做法:② 和 ③ 各自还会重新泄露一次,
而它们都不在切换账号的代码路径上,grep 也找不到。
**身份变化有三条路径,清空也该有三条。**
★ 只碰**数据** store;`uiStore` 的 reset 仍由 App.tsx 负责
(它还要复位窄屏分栏、写信态那些纯界面状态)。
判据:`test/stores/resetAccountData.test.ts`(4 格)。
## ② 鸿蒙管理台门禁**失败开放**(fail-open)
`AdminUsersPage.ets` 原来的条件是 `roleKnown && !this.isAdmin`,
于是 `roleKnown === false`(loadRole() 失败、**身份还没读到**)
落进 else 分支 ⇒ **把完整管理台整个渲染出来**。
一次网络抖动 = 管理入口对所有人可见。
讽刺的是该文件自己的头注释写的就是正确规则
(「不能把读不到当成是管理员」)—— 代码做的正是这条注释禁止的事。
⇒ 改三态:`!roleKnown` 显示「正在确认身份…」、`!isAdmin` 显示墙、
否则管理台。
服务端 `middleware/user.go` 的 `AdminOnly` 仍在,所以**不是越权**;
但非管理员会看到完整用户列表、建号表单、改密入口 ——
属于客户端信息泄露 + 无意义的失败请求风暴。
This commit is contained in:
@ -276,22 +276,34 @@ export function foregroundBundle(hdc) {
|
||||
/**
|
||||
* 拉一份当前 UI 树(`uitest dumpLayout` → 设备上生成 JSON → `cat` 回来 → 解析)。
|
||||
*
|
||||
* dumpLayout 的输出形如 `DumpLayout saved to:/data/local/tmp/layout_<ts>.json`,
|
||||
* 路径是确定的(不是"猜最新的文件"——那样会和别的会话的 dump 抢)。拿到路径
|
||||
* 后 cat 回来 JSON.parse。根是 `{ attributes, children }`,children 递归同形。
|
||||
* ★★ 2026-09-26:**必须显式给输出路径**。
|
||||
*
|
||||
* 原来跑的是裸 `uitest dumpLayout`(不带 `-p`),期望它自己在 stdout 打一行
|
||||
* `DumpLayout saved to:…`。在 HarmonyPhone(HarmonyOS 6.1.1)上实测:
|
||||
* 裸调用 → `DumpLayout failed:Wait for subscribe uitest.broadcast.command.reply timeout`
|
||||
* (非 0 退出、stdout 空 ⇒ 这条判据直接**报错**而不是跳过)
|
||||
* 加 `-p` → `DumpLayout saved to:/data/local/tmp/d.json`(正常)
|
||||
* ⇒ 裸调用依赖的是**自动选名**那条路径,它要等 broadcast 回复;
|
||||
* 给了确定路径就不用等。
|
||||
*
|
||||
* 这不是"设备不在"那种环境缺失,而是**判据自己跑不成** —— 所以必须修,
|
||||
* 否则日历手势那类判据在真机上会一直红。
|
||||
*
|
||||
* 路径自己定 ⇒ 不再从 stdout 反解,也不存在"和别的会话的 dump 抢文件"。
|
||||
*
|
||||
* 抛异常(不返回 null):调用方该知道"dump 跑不成"和"dump 出来是空的"是两回事
|
||||
* —— 前者是环境问题,后者才是断言该管的。判据的 broken 分类靠这个区分。
|
||||
*/
|
||||
export function dumpLayout(hdc) {
|
||||
if (!hdc) throw new Error('hdc 没找到(findHdc() 返回 null)');
|
||||
const r = sh(hdc, ['shell', 'uitest', 'dumpLayout'], 30000);
|
||||
// 每次换一个名字:同一路径反复写会让并发跑的两个判据互相读到对方的中间态。
|
||||
const path = `/data/local/tmp/dsh_dump_${process.pid}_${Date.now()}.json`;
|
||||
const r = sh(hdc, ['shell', 'uitest', 'dumpLayout', '-p', path], 45000);
|
||||
if (r.status !== 0) throw new Error(`dumpLayout 失败:${(r.stderr || r.stdout || '').trim()}`);
|
||||
const m = (r.stdout || '').match(/saved to:(\S+)/);
|
||||
if (!m) throw new Error(`dumpLayout 没返回文件路径:${(r.stdout || '').trim()}`);
|
||||
const path = m[1].trim();
|
||||
const cat = sh(hdc, ['shell', 'cat', path], 20000);
|
||||
if (cat.status !== 0) throw new Error(`cat ${path} 失败:${(cat.stderr || '').trim()}`);
|
||||
// 设备上的临时文件不留(几十次判据跑下来 /data/local/tmp 会攒一堆)。
|
||||
sh(hdc, ['shell', 'rm', '-f', path], 10000);
|
||||
return JSON.parse(cat.stdout);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user