跨端: 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:
2026-09-28 08:27:01 +08:00
parent 044a664cc3
commit 18b148e476
28 changed files with 690 additions and 64 deletions

View File

@ -0,0 +1,37 @@
/**
* 换身份时清空全部账号相关数据 —— **一处实现,三个入口都调它**。
*
* # 为什么需要这个文件
*
* `setActive` 之后 `api/config` 单例里的 `API_BASE` 与 bearer 就翻到了新账号,
* 于是**此后每一个请求都带新账号的凭证**。而各 store 里还留着**旧**账号的数据:
*
* · `mailStore.sent` / `currentMail`
* · `sessionStore.sessions` / `currentSession` / `currentSessionMails`
* · `contactStore.contacts` / `archivedContacts`
*
* 结果就是"在新账号的界面下显示旧账号的邮件",而且**从旧视图发出的写操作
* (归档 / 转发 / 批准权限)改的是新账号**。这类问题不报错、不空屏、
* 肉眼完全看不出来 —— 只能靠一处收口来堵。
*
* # 为什么是三个入口而不是一个
*
* ① 主动切账号(`AccountSwitcher.pick`)
* ② 登出(`authStore.logout`)
* ③ 任意接口 401(`api/client.ts` 的 unauthorized 回调)
*
* 只在 ① 里清是最容易漏的那种做法:② 和 ③ 各自还会重新泄露一次,而它们都不在
* 切换账号的代码路径上,grep 也找不到。**身份变化有三条路径,清空也该有三条。**
*
* ★ 这里只碰**数据** store;`uiStore` 的 `reset` 由 `App.tsx` 负责
* (它还要复位窄屏分栏、写信态那些纯界面状态)。
*/
import { useContactStore } from '../stores/contactStore';
import { useMailStore } from '../stores/mailStore';
import { useSessionStore } from '../stores/sessionStore';
export function resetAccountData(): void {
useMailStore.getState().resetAll();
useSessionStore.getState().resetAll();
useContactStore.getState().resetAll();
}