diff --git a/packages/twenty-front/src/modules/auth/states/isImpersonatingState.ts b/packages/twenty-front/src/modules/auth/states/isImpersonatingState.ts new file mode 100644 index 000000000000..ae898bed59e5 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/states/isImpersonatingState.ts @@ -0,0 +1,24 @@ +import { jwtDecode } from 'jwt-decode'; +import { selector } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { tokenPairState } from './tokenPairState'; + +export const isImpersonatingState = selector({ + key: 'isImpersonatingState', + get: ({ get }) => { + const tokenPair = get(tokenPairState); + + if (!isDefined(tokenPair?.accessOrWorkspaceAgnosticToken?.token)) { + return false; + } + + try { + const decodedToken = jwtDecode<{ isImpersonating: boolean }>( + tokenPair.accessOrWorkspaceAgnosticToken.token, + ); + return decodedToken?.isImpersonating ?? false; + } catch { + return false; + } + }, +}); diff --git a/packages/twenty-front/src/modules/information-banner/components/InformationBannerWrapper.tsx b/packages/twenty-front/src/modules/information-banner/components/InformationBannerWrapper.tsx index 00cb4cb3d2aa..49b5eb7d7dca 100644 --- a/packages/twenty-front/src/modules/information-banner/components/InformationBannerWrapper.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/InformationBannerWrapper.tsx @@ -1,13 +1,16 @@ +import { isImpersonatingState } from '@/auth/states/isImpersonatingState'; import { InformationBannerBillingSubscriptionPaused } from '@/information-banner/components/billing/InformationBannerBillingSubscriptionPaused'; import { InformationBannerEndTrialPeriod } from '@/information-banner/components/billing/InformationBannerEndTrialPeriod'; import { InformationBannerFailPaymentInfo } from '@/information-banner/components/billing/InformationBannerFailPaymentInfo'; import { InformationBannerNoBillingSubscription } from '@/information-banner/components/billing/InformationBannerNoBillingSubscription'; +import { InformationBannerIsImpersonating } from '@/information-banner/components/impersonate/InformationBannerIsImpersonating'; import { InformationBannerReconnectAccountEmailAliases } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases'; import { InformationBannerReconnectAccountInsufficientPermissions } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions'; import { useIsSomeMeteredProductCapReached } from '@/workspace/hooks/useIsSomeMeteredProductCapReached'; import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo'; import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { SubscriptionStatus } from '~/generated-metadata/graphql'; @@ -27,6 +30,7 @@ export const InformationBannerWrapper = () => { WorkspaceActivationStatus.SUSPENDED, ); const isSomeMeteredProductCapReached = useIsSomeMeteredProductCapReached(); + const isImpersonating = useRecoilValue(isImpersonatingState); const displayBillingSubscriptionPausedBanner = isWorkspaceSuspended && subscriptionStatus === SubscriptionStatus.Paused; @@ -54,6 +58,7 @@ export const InformationBannerWrapper = () => { )} {displayFailPaymentInfoBanner && } {displayEndTrialPeriodBanner && } + {isImpersonating && } ); }; diff --git a/packages/twenty-front/src/modules/information-banner/components/impersonate/InformationBannerIsImpersonating.tsx b/packages/twenty-front/src/modules/information-banner/components/impersonate/InformationBannerIsImpersonating.tsx new file mode 100644 index 000000000000..786728200b75 --- /dev/null +++ b/packages/twenty-front/src/modules/information-banner/components/impersonate/InformationBannerIsImpersonating.tsx @@ -0,0 +1,27 @@ +import { useAuth } from '@/auth/hooks/useAuth'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { InformationBanner } from '@/information-banner/components/InformationBanner'; +import { t } from '@lingui/core/macro'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { IconLogout } from 'twenty-ui/display'; + +export const InformationBannerIsImpersonating = () => { + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + const { signOut } = useAuth(); + + if (!isDefined(currentWorkspaceMember)) { + return null; + } + + const impersonatedUser = `${currentWorkspaceMember.name.firstName} ${currentWorkspaceMember.name.lastName} (${currentWorkspaceMember.userEmail})`; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/members/ManageMembersDropdownMenu.tsx b/packages/twenty-front/src/modules/settings/members/ManageMembersDropdownMenu.tsx index 9bcc7fcba4cb..cca7bdcadb3c 100644 --- a/packages/twenty-front/src/modules/settings/members/ManageMembersDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/settings/members/ManageMembersDropdownMenu.tsx @@ -1,3 +1,4 @@ +import { isImpersonatingState } from '@/auth/states/isImpersonatingState'; import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; @@ -5,6 +6,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { t } from '@lingui/core/macro'; +import { useRecoilValue } from 'recoil'; import { IconDotsVertical, IconSpy, IconTrash } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; import { MenuItem } from 'twenty-ui/navigation'; @@ -24,7 +26,9 @@ export const ManageMembersDropdownMenu = ({ onImpersonate, }: ManageMembersDropdownMenuProps) => { const { closeDropdown } = useCloseDropdown(); - const canImpersonate = useHasPermissionFlag(PermissionFlagType.IMPERSONATE); + const isImpersonating = useRecoilValue(isImpersonatingState); + const canImpersonate = + useHasPermissionFlag(PermissionFlagType.IMPERSONATE) && !isImpersonating; return (