176 lines
5.6 KiB
TypeScript
176 lines
5.6 KiB
TypeScript
/**
|
||
* 持久化中间件基础设施。
|
||
*
|
||
* 为了平滑迁移,这里把旧实现直接读写 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 };
|