diff --git a/src/app.tsx b/src/app.tsx index 448929f1..abd7a1b7 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,11 +1,12 @@ import { userSettingsHelperAtom } from '@/atoms/settings'; -import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user'; +import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user'; import { setAtomStorage } from '@/atoms/utils'; import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings'; import { COLOR_PRIMARY } from '@/config/theme/constants'; import { queryClusterList } from '@/pages/cluster-management/apis'; import { ProviderValueMap } from '@/pages/cluster-management/config'; import { queryResourceEvents } from '@/pages/usage/apis/resource'; +import { getGPUStackPlugin } from '@/plugins'; import { enterprisePluginReady } from '@/plugins/enterprise-ready'; import { GPUStackPluginManager } from '@/plugins/manager'; import { requestConfig } from '@/request-config'; @@ -160,6 +161,36 @@ export async function getInitialState(): Promise<{ getUpdateCheck(); fetchSystemConfig(); } + // Only commit a substantive user object. A truthy-but-empty + // `data` (e.g. server responded 200 with an empty body) would + // otherwise look like "logged in" to every `currentUser` + // reader and the access seam — break out instead and let the + // caller treat the request as failed. + if (data && typeof data === 'object' && Object.keys(data).length > 0) { + // Commit the identity to atom storage (and so to localStorage) + // before returning. The access function — memoized on + // `initialState` and run once per commit — reads identity from + // localStorage; without this preemptive write the predicate + // sees the prior session's identity on its first evaluation + // after login, and stays stale until the next identity change + // (which usually doesn't come without a manual refresh). + try { + setAtomStorage(userAtom, data); + } catch (err) { + console.error('userAtom commit error:', err); + } + // Fire `onUserFetched` so plugins maintaining identity-scoped + // caches can seed them under the new identity before any + // caller commits this user to `initialState`. Errors here are + // swallowed and logged — fetchUserInfo must still return. + try { + await getGPUStackPlugin()?.login?.onUserFetched?.(data, { + request: umiRequest + }); + } catch (err) { + console.error('onUserFetched plugin hook error:', err); + } + } return data; } catch (error: any) { const data = error?.response?.data; diff --git a/src/plugins/types.ts b/src/plugins/types.ts index d08b4ede..6b7b1251 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -57,6 +57,26 @@ export interface LoginPlugin { userInfo: any, ctx: { request: (url: string, options?: any) => Promise } ) => Promise | string | null | undefined; + /** + * Lifecycle hook fired inside `fetchUserInfo` after the server + * confirms identity but before any caller (boot path, LoginForm) + * commits that identity to `initialState`. + * + * The host's access function is memoized on `initialState` and + * runs exactly once per commit. Plugins that maintain + * identity-scoped caches (e.g. an org context cache the access + * predicate reads from) MUST seed those caches synchronously + * here — any work that happens after the caller's + * `setInitialState` won't influence the access predicate until + * the next identity change. + * + * Errors thrown from the hook are swallowed and logged; they + * never block fetchUserInfo. + */ + onUserFetched?: ( + userInfo: any, + ctx: { request: (url: string, options?: any) => Promise } + ) => Promise | void; } /**