feat(login): add onUserFetched plugin hook
The host's access function is memoized on `initialState` and runs
exactly once per commit. Plugins that maintain identity-scoped
caches the access predicate reads from (e.g. an org context cache)
had no way to seed those caches synchronously before the caller's
`setInitialState({currentUser: ...})` fired — any post-commit hydrate
couldn't widen the predicate, leaving the sidebar in a stale view
until the next identity change.
Add `LoginPlugin.onUserFetched(userInfo, ctx)` and call it inside
`fetchUserInfo` after the server confirms identity but before
returning. Also commit the identity to `userAtom` storage here so
localStorage's identity marker is in lockstep with whatever caches
the plugin seeds — the predicate's first evaluation then sees a
consistent view rather than the prior session's data.
Errors thrown from the hook are swallowed and logged; they never
block fetchUserInfo.
This commit is contained in:
+32
-1
@@ -1,11 +1,12 @@
|
|||||||
import { userSettingsHelperAtom } from '@/atoms/settings';
|
import { userSettingsHelperAtom } from '@/atoms/settings';
|
||||||
import { GPUStackVersionAtom, UpdateCheckAtom } from '@/atoms/user';
|
import { GPUStackVersionAtom, UpdateCheckAtom, userAtom } from '@/atoms/user';
|
||||||
import { setAtomStorage } from '@/atoms/utils';
|
import { setAtomStorage } from '@/atoms/utils';
|
||||||
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
import { DEFAULT_ENTER_PAGE, GPUSTACK_API_BASE_URL } from '@/config/settings';
|
||||||
import { COLOR_PRIMARY } from '@/config/theme/constants';
|
import { COLOR_PRIMARY } from '@/config/theme/constants';
|
||||||
import { queryClusterList } from '@/pages/cluster-management/apis';
|
import { queryClusterList } from '@/pages/cluster-management/apis';
|
||||||
import { ProviderValueMap } from '@/pages/cluster-management/config';
|
import { ProviderValueMap } from '@/pages/cluster-management/config';
|
||||||
import { queryResourceEvents } from '@/pages/usage/apis/resource';
|
import { queryResourceEvents } from '@/pages/usage/apis/resource';
|
||||||
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
|
import { enterprisePluginReady } from '@/plugins/enterprise-ready';
|
||||||
import { GPUStackPluginManager } from '@/plugins/manager';
|
import { GPUStackPluginManager } from '@/plugins/manager';
|
||||||
import { requestConfig } from '@/request-config';
|
import { requestConfig } from '@/request-config';
|
||||||
@@ -160,6 +161,36 @@ export async function getInitialState(): Promise<{
|
|||||||
getUpdateCheck();
|
getUpdateCheck();
|
||||||
fetchSystemConfig();
|
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;
|
return data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const data = error?.response?.data;
|
const data = error?.response?.data;
|
||||||
|
|||||||
@@ -57,6 +57,26 @@ export interface LoginPlugin {
|
|||||||
userInfo: any,
|
userInfo: any,
|
||||||
ctx: { request: <T = any>(url: string, options?: any) => Promise<T> }
|
ctx: { request: <T = any>(url: string, options?: any) => Promise<T> }
|
||||||
) => Promise<string | null | undefined> | string | null | undefined;
|
) => Promise<string | null | undefined> | 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: <T = any>(url: string, options?: any) => Promise<T> }
|
||||||
|
) => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user