跨端: 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:
@ -8,7 +8,7 @@ import {
|
||||
uploadAppearanceImage,
|
||||
type AppearanceAuth
|
||||
} from '../lib/appearance';
|
||||
import { useAccountStore } from './accountStore';
|
||||
import { useAccountStore, isAggregate } from './accountStore';
|
||||
import { useBackgroundStore } from './backgroundStore';
|
||||
import { useThemeStore } from './themeStore';
|
||||
|
||||
@ -36,8 +36,27 @@ interface AppearanceSyncState {
|
||||
push: () => Promise<void>;
|
||||
}
|
||||
|
||||
/** 上一次成功上传的壁纸(data URL)。相同就不重复上传几 MB。 */
|
||||
let lastUploadedImage = '';
|
||||
/**
|
||||
* 上一次成功上传的壁纸(data URL),**按账号分桶**。
|
||||
*
|
||||
* ★ 为什么必须带账号维度:这是本模块唯一没做账号隔离的状态,而它恰好是最不能
|
||||
* 漏的那一个。A 账号设壁纸 X(上传成功 ⇒ 记下 "A"→X)→ 切到 B 账号,而 B 的
|
||||
* 服务端记录是 `saved:false` ⇒ `pull()` 走"把本地这份推上去"的分支 ⇒ 但
|
||||
* `push()` 的跳过判据被 **A** 留下的标记满足 ⇒ **B 的壁纸永远传不上去**,
|
||||
* 而状态仍被置为 `'synced'`。现象是"B 的壁纸在其它所有设备上都缺,
|
||||
* 界面上却显示已同步"。
|
||||
*
|
||||
* 本来这个文件把 localStorage 键按账号隔离了(见 `lib/appearance.ts`),
|
||||
* 漏的只有这个内存变量。用 `Map` 而不是单串,是因为它的生命周期就等于
|
||||
* 一次运行期,而账号数很小,不需要额外持久化。
|
||||
*/
|
||||
const lastUploadedImage = new Map<string, string>();
|
||||
|
||||
/** 当前身份的稳定键。聚合视图下用固定值(聚合本来就不该单独上传)。 */
|
||||
function currentAccountKey(): string {
|
||||
const s = useAccountStore.getState();
|
||||
return isAggregate(s) ? '__aggregate__' : s.activeId;
|
||||
}
|
||||
let pushTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function currentAuth(): AppearanceAuth | null {
|
||||
@ -84,7 +103,7 @@ export const useAppearanceSync = create<AppearanceSyncState>((set) => ({
|
||||
if (remote.snapshot.bgKind === 'preset') bg.setPreset(remote.snapshot.bgPresetId);
|
||||
else if (remote.snapshot.bgKind === 'none') bg.setKind('none');
|
||||
else if (remote.snapshot.bgKind === 'image' && remote.imageDataUrl) bg.setImage(remote.imageDataUrl);
|
||||
if (remote.imageDataUrl) lastUploadedImage = remote.imageDataUrl;
|
||||
if (remote.imageDataUrl) lastUploadedImage.set(currentAccountKey(), remote.imageDataUrl);
|
||||
bg.setDim(remote.snapshot.bgDim);
|
||||
bg.setBlur(remote.snapshot.bgBlur);
|
||||
} finally {
|
||||
@ -101,6 +120,7 @@ export const useAppearanceSync = create<AppearanceSyncState>((set) => ({
|
||||
}
|
||||
const theme = useThemeStore.getState().pref;
|
||||
const bg = useBackgroundStore.getState();
|
||||
const accountKey = currentAccountKey();
|
||||
const payload = payloadFromLocal({
|
||||
theme,
|
||||
kind: bg.kind,
|
||||
@ -112,9 +132,9 @@ export const useAppearanceSync = create<AppearanceSyncState>((set) => ({
|
||||
|
||||
let ok = await pushAppearance(auth, payload);
|
||||
// 壁纸本体单独传:几 MB 的图不该每次都跟着 PUT 走,只在换图时传一次。
|
||||
if (ok && bg.kind === 'image' && bg.imageDataUrl && bg.imageDataUrl !== lastUploadedImage) {
|
||||
if (ok && bg.kind === 'image' && bg.imageDataUrl && bg.imageDataUrl !== lastUploadedImage.get(accountKey)) {
|
||||
const uploaded = await uploadAppearanceImage(auth, bg.imageDataUrl);
|
||||
if (uploaded) lastUploadedImage = bg.imageDataUrl;
|
||||
if (uploaded) lastUploadedImage.set(accountKey, bg.imageDataUrl);
|
||||
else ok = false;
|
||||
}
|
||||
set(ok ? { status: 'synced', lastSyncedAt: Date.now() } : { status: 'pending' });
|
||||
|
||||
Reference in New Issue
Block a user