From ecd90b78b9733bbe26f61e0d873349a9eb90533e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Wed, 17 Jun 2026 23:01:30 +0200 Subject: [PATCH] fix(front): stop impersonation from corrupting the impersonator's profile name (#21757) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem A customer reported that after impersonating another user, **their own account's first/last name had been permanently changed to the impersonated user's name** (persisted in the DB, surviving logout), and their actions showed up under the wrong "Updated by". The "Updated by = impersonated user" part is expected (while impersonating you genuinely act as that user). The real bug is the **durable overwrite of the impersonator's profile name**. ## Root cause About a week ago `currentUserState` became localStorage-backed with `getOnInit: true` (`currentWorkspaceMemberState` already was). Impersonation swaps the auth token and does a full reload, but — unlike sign-out — it never cleared those cached identity keys. So after the reload: 1. The atoms hydrate **synchronously from localStorage with the previous identity** (e.g. the impersonated user, on stop), and because `currentUser` is now non-null the authenticated UI renders immediately with that stale identity. 2. The network `loadCurrentUser` then corrects `currentUser` / `currentWorkspaceMember` **in place**. If **Settings → Profile** was mounted across that in-place identity flip, `NameFields` — which seeds local `useState` from `currentWorkspaceMember` once and auto-saves on change — read the stale name as a pending edit and debounce-saved it onto the **now-current** workspace member, persisting one user's name onto another. Read-only caches (object metadata, permissions) tolerate the same staleness because nothing writes them back — they're only ever overwritten by the network. `NameFields` is the one consumer that *persists* a cached identity value, which is what turns a transient stale read into a durable write. ## Fix Two small, complementary layers: - **`useImpersonationSession`** — clear the cached session identity (`clearSessionLocalStorageKeys()`) on both `startImpersonating` and `stopImpersonating`, before the reload. The reload then re-bootstraps from a clean slate for the correct user (and the brief stale-**permissions** flash goes away too). The admin's token stash lives in `sessionStorage` and is untouched; `tokenPairState` has its own key and is not in the cleared set. - **`NameFields`** — re-seed the inputs when the workspace-member **identity** changes, so an identity swap is never mistaken for a user edit. This closes the underlying footgun regardless of how the identity changes. ## Testing - Added `NameFields.test.tsx`: swapping `currentWorkspaceMemberState` to a different member must **not** trigger `updateWorkspaceMemberSettings`, while a genuine user edit still saves. Verified the test **fails without** the `NameFields` fix (it writes the previous member's name onto the new member) and **passes with** it. - `nx typecheck twenty-front`, `oxlint --type-aware`, and `oxfmt --check` all pass on the changed files; full `twenty-front` Jest suite green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_015MVW3gg7CVq5oR572ctisx --- _Generated by [Claude Code](https://claude.ai/code/session_015MVW3gg7CVq5oR572ctisx)_ Review in cubic --------- Co-authored-by: Claude --- .../src/modules/auth/hooks/useImpersonationSession.ts | 3 +++ .../src/pages/settings/profile/SettingsProfile.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/auth/hooks/useImpersonationSession.ts b/packages/twenty-front/src/modules/auth/hooks/useImpersonationSession.ts index 17dfd9cff3..89fb135d94 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useImpersonationSession.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useImpersonationSession.ts @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { useAuth } from '@/auth/hooks/useAuth'; import { tokenPairState } from '@/auth/states/tokenPairState'; +import { clearSessionLocalStorageKeys } from '@/auth/utils/clearSessionLocalStorageKeys'; import { type AuthTokenPair } from '~/generated-metadata/graphql'; const IMPERSONATION_SESSION_KEY = 'impersonation_original_session'; @@ -47,6 +48,7 @@ export const useImpersonationSession = () => { throw error; } + clearSessionLocalStorageKeys(); reloadWithSession(targetPath); }, [store, getAuthTokensFromLoginToken], @@ -75,6 +77,7 @@ export const useImpersonationSession = () => { sessionStorage.removeItem(IMPERSONATION_SESSION_KEY); store.set(tokenPairState.atom, session.tokenPair); + clearSessionLocalStorageKeys(); reloadWithSession(session.returnPath); }, [store, signOut]); diff --git a/packages/twenty-front/src/pages/settings/profile/SettingsProfile.tsx b/packages/twenty-front/src/pages/settings/profile/SettingsProfile.tsx index 840f858b70..b301f4b711 100644 --- a/packages/twenty-front/src/pages/settings/profile/SettingsProfile.tsx +++ b/packages/twenty-front/src/pages/settings/profile/SettingsProfile.tsx @@ -57,7 +57,7 @@ export const SettingsProfile = () => { title={t`Name`} description={t`Your name as it will be displayed`} /> - +