fix(frontend): harden auth request layer per web guide

- fix double-unwrap bug in useLogin prefs sync (theme sync silently broken)
- request layer: 401 single-flight refresh queue + debounced single redirect
  with return-url, cancel-aware silent passthrough, skipErrorHandler opt-out,
  typed blob download (replace bare fetch export in QueriesPage)
- useFetchList: AbortController + version guard + error state (three states)
- ErrorBoundary with chunk-load auto recovery (reload once)
- permission single source: derive route guard from menu constants, drop
  hand-maintained routePermissionMap; TicketDetailPage uses unified
  hasActionPermission instead of inline role string comparison
- namespaced localStorage keys (opsxsql.*) with read-old-write-new migration
- fix pre-existing tsc errors (InstancePage/WorkflowPage/QueriesPage/
  PlatformConfigPage/RedisDetailPage) - typecheck now passes clean
This commit is contained in:
Logan.Li committed 2026-10-26 17:12:29 +08:00
1 parent 0fcdbc5f6c
commit d5b8b8116c
19 files changed
+502 -110

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
/**
* 统一的 localStorage 命名空间与迁移层。
*
* 所有持久化 key 一律从 STORAGE_KEYS 取值(`<app>.<域>.<名>` 命名空间),
* 禁止在业务代码里手写字符串 key。历史无命名空间 key 仅在此处做
* 「读旧写新」迁移,迁移期结束后删除 LEGACY_KEYS。
*/
const read = (key: string): string | null => {
try {
return localStorage.getItem(key);
} catch {
return null;
}
};
const write = (key: string, value: string): void => {
try {
localStorage.setItem(key, value);
} catch {
/* 存储不可用(隐私模式等)时静默降级为内存态 */
}
};
const remove = (key: string): void => {
try {
localStorage.removeItem(key);
} catch {
/* noop */
}
};
export const STORAGE_KEYS = {
token: 'opsxsql.auth.token',
refreshToken: 'opsxsql.auth.refresh',
theme: 'opsxsql.theme.mode',
loginStyle: 'opsxsql.ui.loginStyle',
queryFavorites: 'opsxsql.query.favorites',
querySideWidth: 'opsxsql.query.sideWidth',
chunkReloaded: 'opsxsql.app.chunkReloaded',
} as const;
/** 历史无命名空间 key → 新 key(仅用于一次性迁移读取) */
const LEGACY_KEYS: Partial<Record<keyof typeof STORAGE_KEYS, string>> = {
token: 'token',
theme: 'dg-theme',
loginStyle: 'login-style',
queryFavorites: 'query_favorites',
querySideWidth: 'query-side-width',
};
/** 读取(新 key 优先,回退旧 key 并完成迁移写入) */
export function readStorage(key: keyof typeof STORAGE_KEYS): string | null {
const current = read(STORAGE_KEYS[key]);
if (current !== null) return current;
const legacyKey = LEGACY_KEYS[key];
if (legacyKey) {
const legacy = read(legacyKey);
if (legacy !== null) {
write(STORAGE_KEYS[key], legacy);
remove(legacyKey);
return legacy;
}
}
return null;
}
/** 写入(只写新 key) */
export function writeStorage(key: keyof typeof STORAGE_KEYS, value: string): void {
write(STORAGE_KEYS[key], value);
}
/** 删除(新旧 key 一起清理) */
export function removeStorage(key: keyof typeof STORAGE_KEYS): void {
remove(STORAGE_KEYS[key]);
const legacyKey = LEGACY_KEYS[key];
if (legacyKey) remove(legacyKey);
}