Files
gongxue-base/apps/admin/src/store/middleware/persist.ts

176 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 持久化中间件基础设施。
*
* 为了平滑迁移,这里把旧实现直接读写 localStorage 的 key
* token / user / permissions / gongxue-route-dock包装成 zustand
* persist 的 StateStorage保证迁移前后数据格式兼容。
*/
import { createJSONStorage, type StateStorage } from 'zustand/middleware';
import type { AppPersistedState, DockTab } from '../app/appTypes';
import type { PermissionPersistedState } from '../permission/permissionTypes';
import type { UserInfo, UserPersistedState } from '../user/userTypes';
export const AUTH_STORAGE_NAME = 'gongxue-auth';
export const LEGACY_TOKEN_KEY = 'token';
export const LEGACY_USER_KEY = 'user';
export const PERMISSION_STORAGE_NAME = 'permissions';
export const APP_UI_STORAGE_NAME = 'gongxue-app-ui';
export const LEGACY_DOCK_STORAGE_KEY = 'gongxue-route-dock';
export const SETTINGS_STORAGE_NAME = 'gongxue-settings';
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function isDockTab(value: unknown): value is DockTab {
return (
isRecord(value) &&
typeof value.key === 'string' &&
value.key.startsWith('/') &&
typeof value.label === 'string'
);
}
/**
* 用户会话持久化:继续使用旧的 `token` / `user` 两个 key
* 保持与后端、既有代码及浏览器缓存格式一致。
*/
const legacyAuthStorage: StateStorage = {
getItem: () => {
const token = localStorage.getItem(LEGACY_TOKEN_KEY);
const rawUser = localStorage.getItem(LEGACY_USER_KEY);
if (token === null && rawUser === null) return null;
let user: UserInfo | null = null;
if (rawUser !== null) {
try {
const parsed: unknown = JSON.parse(rawUser);
user = isRecord(parsed) ? (parsed as UserInfo) : null;
} catch {
user = null;
}
}
return JSON.stringify({ state: { token, user }, version: 1 });
},
setItem: (_name, value) => {
try {
const persisted = JSON.parse(value) as { state?: UserPersistedState };
const { token, user } = persisted.state ?? {};
if (token) {
localStorage.setItem(LEGACY_TOKEN_KEY, token);
} else {
localStorage.removeItem(LEGACY_TOKEN_KEY);
}
if (user) {
localStorage.setItem(LEGACY_USER_KEY, JSON.stringify(user));
} else {
localStorage.removeItem(LEGACY_USER_KEY);
}
} catch {
// 持久化写入失败不应影响应用运行
}
},
removeItem: () => {
localStorage.removeItem(LEGACY_TOKEN_KEY);
localStorage.removeItem(LEGACY_USER_KEY);
},
};
/**
* 权限持久化:兼容旧格式(原始 JSON 数组)与 zustand persist 格式。
* 无论磁盘上是什么状态,恢复后一律为 `unknown`,保持 fail-closed
* 直到 `/auth/profile` 校验成功。
*/
const legacyPermissionStorage: StateStorage = {
getItem: () => {
const raw = localStorage.getItem(PERMISSION_STORAGE_NAME);
if (!raw) return null;
try {
const parsed: unknown = JSON.parse(raw);
if (Array.isArray(parsed)) {
return JSON.stringify({
state: { permissions: parsed.filter(isString), status: 'unknown' },
version: 1,
});
}
if (isRecord(parsed) && isRecord(parsed.state)) {
const permissions = Array.isArray(parsed.state.permissions)
? parsed.state.permissions.filter(isString)
: [];
return JSON.stringify({
state: { permissions, status: 'unknown' },
version: 1,
});
}
} catch {
// 损坏的缓存按无权限处理
}
return null;
},
setItem: (_name, value) => {
try {
const persisted = JSON.parse(value) as { state?: PermissionPersistedState };
const permissions = Array.isArray(persisted.state?.permissions)
? persisted.state.permissions.filter(isString)
: [];
localStorage.setItem(PERMISSION_STORAGE_NAME, JSON.stringify(permissions));
} catch {
// 忽略损坏数据
}
},
removeItem: () => {
localStorage.removeItem(PERMISSION_STORAGE_NAME);
},
};
/**
* 应用 UI 状态持久化:新 key `gongxue-app-ui`
* 首次读取时自动迁移旧 key `gongxue-route-dock` 中已打开的页签。
*/
const appUiStorage: StateStorage = {
getItem: (name) => {
const current = localStorage.getItem(name);
if (current) return current;
const legacy = localStorage.getItem(LEGACY_DOCK_STORAGE_KEY);
if (!legacy) return null;
try {
const parsed: unknown = JSON.parse(legacy);
const routeDockTabs = Array.isArray(parsed) ? parsed.filter(isDockTab) : [];
return JSON.stringify({ state: { routeDockTabs, sidebarCollapsed: false }, version: 1 });
} catch {
return null;
}
},
setItem: (name, value) => {
try {
localStorage.setItem(name, value);
} catch {
// 持久化失败不应影响应用运行
}
},
removeItem: (name) => {
try {
localStorage.removeItem(name);
} catch {
// 持久化失败不应影响应用运行
}
},
};
/** 会话 Store 使用的 persist storage兼容旧 token/user key */
export const authPersistStorage = createJSONStorage(() => legacyAuthStorage);
/** 权限 Store 使用的 persist storage兼容旧 permissions key */
export const permissionPersistStorage = createJSONStorage(() => legacyPermissionStorage);
/** 应用 UI Store 使用的 persist storage含旧 RouteDock key 迁移) */
export const appUiPersistStorage = createJSONStorage(() => appUiStorage);
export type { AppPersistedState, PermissionPersistedState, UserPersistedState };