From 18b148e4761bf5bdfc64c686996e094f0376352c Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 28 Sep 2026 08:27:01 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=A8=E7=AB=AF:=20fix(=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF)=20=E6=8D=A2=E8=BA=AB=E4=BB=BD=E5=BF=85=E9=A1=BB?= =?UTF-8?q?=E6=B8=85=E5=85=A8=E9=83=A8=E8=B4=A6=E5=8F=B7=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=20=EF=BC=8B=20=E9=B8=BF=E8=92=99=E7=AE=A1=E7=90=86=E5=8F=B0?= =?UTF-8?q?=E9=97=A8=E7=A6=81=E6=94=B9=E4=B8=89=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两份审查报告(`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` 仍在,所以**不是越权**; 但非管理员会看到完整用户列表、建号表单、改密入口 —— 属于客户端信息泄露 + 无意义的失败请求风暴。 --- .../src/components/AccountSwitcher.tsx | 16 ++- .../src/components/AdminUsersPage.tsx | 17 ++- .../src/components/BackgroundPicker.tsx | 11 +- .../electron/src/components/CalendarView.tsx | 29 +++- .../electron/src/components/ComposePage.tsx | 15 +- client/electron/src/components/KeyPanel.tsx | 13 +- client/electron/src/components/MailView.tsx | 49 ++++++- client/electron/src/lib/resetAccountData.ts | 37 +++++ client/electron/src/stores/appearanceSync.ts | 32 ++++- client/electron/src/stores/authStore.ts | 14 +- client/electron/src/stores/contactStore.ts | 18 ++- client/electron/src/stores/mailStore.ts | 15 ++ client/electron/src/stores/sessionStore.ts | 21 ++- .../electron/test/cross-client-theme.test.mjs | 26 +++- .../electron/test/harmony-imageprep.test.mjs | 14 +- client/electron/test/lib/harmony-device.mjs | 26 +++- .../test/stores/resetAccountData.test.ts | 129 ++++++++++++++++++ .../harmony/entry/src/main/ets/api/Logout.ets | 22 +++ .../src/main/ets/common/ComposeIntent.ets | 7 + .../entry/src/main/ets/common/MailStore.ets | 18 ++- .../entry/src/main/ets/common/Theme.ets | 85 +++++++++--- .../main/ets/entryability/EntryAbility.ets | 31 +++++ .../src/main/ets/pages/AdminUsersPage.ets | 34 ++++- .../entry/src/main/ets/pages/CalendarPage.ets | 8 +- .../src/main/ets/pages/MailDetailPage.ets | 9 +- .../entry/src/main/ets/pages/MainPage.ets | 36 ++++- .../entry/src/main/ets/pages/SessionsPage.ets | 2 +- .../entry/src/main/ets/pages/SettingsPage.ets | 20 ++- 28 files changed, 690 insertions(+), 64 deletions(-) create mode 100644 client/electron/src/lib/resetAccountData.ts create mode 100644 client/electron/test/stores/resetAccountData.test.ts diff --git a/client/electron/src/components/AccountSwitcher.tsx b/client/electron/src/components/AccountSwitcher.tsx index b5835cb..5742bd2 100644 --- a/client/electron/src/components/AccountSwitcher.tsx +++ b/client/electron/src/components/AccountSwitcher.tsx @@ -12,6 +12,7 @@ import { useEffect, useRef, useState } from 'react'; import { AGGREGATE_ID, isUsableAccount } from '../lib/accounts'; +import { resetAccountData } from '../lib/resetAccountData'; import { useAccountStore } from '../stores/accountStore'; import { useMailStore } from '../stores/mailStore'; import { useUIStore } from '../stores/uiStore'; @@ -55,9 +56,20 @@ export default function AccountSwitcher() { const pick = async (id: string) => { setOpen(false); + /* + * ★ 切账号**必须先清全部账号数据**,再重取。 + * + * `setActive` 会 `syncAuth()` 翻 `api/config` 单例里的 API_BASE 与 bearer, + * 从这一刻起 `api.getSent()` / `api.getMail()` / `api.archiveContact()` 全带 + * **新**账号的凭证。而 mailStore 的 sent/currentMail、sessionStore 的 + * sessions/currentSession(+Mails)、contactStore 的 contacts 全是**旧**账号的 —— + * 留着就会"在新账号的界面下显示旧账号的邮件",而从这些陈旧视图点归档/转发 + * 改的是**新**账号。不报错、不空屏,肉眼完全看不出来,所以只能靠这里堵。 + * + * 登出与 401 走的是同一套清空(`App.tsx` 与 `api/client.ts` 的 unauthorized 回调)。 + */ + resetAccountData(); await setActive(id); - // 切账号后**必须重取**:收件箱是按账号返回的,不重取就会看到上一个账号的信。 - // 这条在聚合⇄单账号之间同样成立(聚合要并发问多个账号)。 await fetchInbox('all'); }; diff --git a/client/electron/src/components/AdminUsersPage.tsx b/client/electron/src/components/AdminUsersPage.tsx index b54946e..358f7ba 100644 --- a/client/electron/src/components/AdminUsersPage.tsx +++ b/client/electron/src/components/AdminUsersPage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import * as api from '../api/client'; import type { AdminScopes, User } from '../types'; import { CheckIcon, LockIcon, UsersIcon, ChevronRightIcon, KeyIcon, BotIcon, CpuIcon } from './icons'; @@ -89,9 +89,22 @@ export default function AdminUsersPage() { } }; + /* + * 提示条自动消失。句柄存进 ref 并在卸载时清掉:原来直接 `setTimeout` 不管, + * 组件卸载后那个回调仍会跑(状态写在死组件上是纯粹的脏行为),而且连点几次 + * 会留下多个待触发的定时器,先触发的会把后触发的提示提前吃掉。 + */ + const noticeTimer = useRef | null>(null); + useEffect(() => { + return () => { + if (noticeTimer.current) clearTimeout(noticeTimer.current); + }; + }, []); + const flash = (msg: string) => { setNotice(msg); - setTimeout(() => setNotice(null), 2500); + if (noticeTimer.current) clearTimeout(noticeTimer.current); + noticeTimer.current = setTimeout(() => setNotice(null), 2500); }; return ( diff --git a/client/electron/src/components/BackgroundPicker.tsx b/client/electron/src/components/BackgroundPicker.tsx index ca8a8fa..4e6a918 100644 --- a/client/electron/src/components/BackgroundPicker.tsx +++ b/client/electron/src/components/BackgroundPicker.tsx @@ -115,10 +115,17 @@ export default function BackgroundPicker() { presetId === p.id ? 'border-blue-500 ring-1 ring-blue-500' : 'border-gray-300' }`} > - {/* 用与全屏背景同一份 CSS 变量渲染缩略图 —— 预览必然等于结果 */} + {/* + * 用与全屏背景同一份 CSS 变量渲染缩略图 —— 预览必然等于结果。 + * + * ★ 这里**不能**写 `style={{ backgroundImage: 'var(--bg-image)' }}`: + * 内联样式优先于类,而当 `kind==='image'` 时 `:root` 上被设了 + * `--bg-image: url("用户那张图")`(`backgroundStore.ts:262`)⇒ + * **六个预设格子全都显示同一张照片**,而用户正在这一步里挑预设。 + * 预设类自己就定义了 `--bg-image`(`index.css:783+`),让它生效即可。 + */}