diff --git a/frontend/src/lib/components/ForkWorkspaceBanner.svelte b/frontend/src/lib/components/ForkWorkspaceBanner.svelte index c37370bcb0..9ab0dfa744 100644 --- a/frontend/src/lib/components/ForkWorkspaceBanner.svelte +++ b/frontend/src/lib/components/ForkWorkspaceBanner.svelte @@ -1,5 +1,5 @@ -{#if isFork} +{#if showBanner}
diff --git a/frontend/src/lib/stores.ts b/frontend/src/lib/stores.ts index 3761e291dd..0058cc8dd0 100644 --- a/frontend/src/lib/stores.ts +++ b/frontend/src/lib/stores.ts @@ -17,6 +17,11 @@ import { DEFAULT_HUB_BASE_URL } from './hub' import type { DbManagerUriState } from './components/dbManagerDrawerModel.svelte' export interface UserExt { + // Workspace this membership was fetched for. `$workspaceStore` flips + // synchronously on a switch while the new `whoami` is still in flight, so a + // consumer whose behavior depends on the role must compare this against the + // active workspace rather than read a role that still describes the previous one. + workspace_id: string email: string name?: string username: string diff --git a/frontend/src/lib/user.ts b/frontend/src/lib/user.ts index aaca893636..cbeb0ccccf 100644 --- a/frontend/src/lib/user.ts +++ b/frontend/src/lib/user.ts @@ -4,15 +4,16 @@ import type { UserExt } from './stores.js' export async function getUserExt(workspace: string): Promise { try { const user = await UserService.whoami({ workspace }) - return mapUserToUserExt(user) + return mapUserToUserExt(user, workspace) } catch (error) { return undefined } } -function mapUserToUserExt(user: User): UserExt { +function mapUserToUserExt(user: User, workspace: string): UserExt { const ext: UserExt = { ...user, + workspace_id: workspace, groups: user.groups!, pgroups: user.groups!.map((x) => `g/${x}`) } diff --git a/frontend/src/routes/(root)/(logged)/+layout.svelte b/frontend/src/routes/(root)/(logged)/+layout.svelte index a88184ed62..ecb7b6c39b 100644 --- a/frontend/src/routes/(root)/(logged)/+layout.svelte +++ b/frontend/src/routes/(root)/(logged)/+layout.svelte @@ -307,6 +307,13 @@ } } const user = await getUserExt(workspace) + // Every workspace change starts a fetch without cancelling the one before it, + // so a slow response can land after a faster one for the workspace the user + // has since moved to. The store must describe the active workspace: letting a + // superseded response write would leave every role gate reading the one we left. + if ($workspaceStore !== workspace) { + return + } if (!deepEqual(user, $userStore)) { userStore.set(user) } @@ -628,8 +635,15 @@ timeout = undefined } else if (!u) { timeout = setTimeout(async () => { - if (!$userStore && $workspaceStore) { - $userStore = await getUserExt($workspaceStore) + const ws = $workspaceStore + if (!$userStore && ws) { + const user = await getUserExt(ws) + // Recovers the workspace that was left without a role. A switch + // mid-flight has already started the fetch for the new one, so this + // answer describes the workspace we left. + if ($workspaceStore === ws) { + $userStore = user + } } }, 5000) } diff --git a/frontend/src/routes/(root)/+layout.svelte b/frontend/src/routes/(root)/+layout.svelte index 6fbffda21d..1f7a813eac 100644 --- a/frontend/src/routes/(root)/+layout.svelte +++ b/frontend/src/routes/(root)/+layout.svelte @@ -130,10 +130,18 @@ if ($userStore) { console.log(`Welcome back ${$userStore.username} to ${$workspaceStore}`) } else { - $userStore = await getUserExt($workspaceStore) - if (!$userStore) { + const ws = $workspaceStore + const user = await getUserExt(ws) + // A switch mid-flight means this answers for the workspace we left, and + // that switch has already started the fetch answering for the active + // one: neither this role nor its failure describes where we are now. + if ($workspaceStore !== ws) { + return + } + if (!user) { throw Error('Not logged in') } + $userStore = user } } else { if ( @@ -240,7 +248,11 @@ if (workspace && user) { const newUser = await getUserExt(workspace) - if (!deepEqual(newUser, $userStore)) { + // Refreshes the workspace that was active when the tick started; a + // switch mid-flight makes this answer describe the one we left. + if ($workspaceStore !== workspace) { + console.debug('workspace changed during user refresh, dropping') + } else if (!deepEqual(newUser, $userStore)) { userStore.set(newUser) console.info('refreshed user') } else {