impersonate regression fix (#10306)

This commit is contained in:
nitin
2025-02-19 16:35:40 +05:30
committed by GitHub
parent 861face2a8
commit d5b04fae28
5 changed files with 122 additions and 34 deletions

View File

@ -1,13 +1,11 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth';
import { useIsLogged } from '@/auth/hooks/useIsLogged'; import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState'; import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
import { useNavigateApp } from '~/hooks/useNavigateApp'; import { useNavigateApp } from '~/hooks/useNavigateApp';
@ -17,15 +15,9 @@ export const VerifyEffect = () => {
const errorMessage = searchParams.get('errorMessage'); const errorMessage = searchParams.get('errorMessage');
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const isLogged = useIsLogged(); const isLogged = useIsLogged();
const navigate = useNavigateApp(); const navigate = useNavigateApp();
const { verifyLoginToken } = useVerifyLogin();
const { getAuthTokensFromLoginToken } = useAuth();
const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState(
isAppWaitingForFreshObjectMetadataState,
);
useEffect(() => { useEffect(() => {
if (isDefined(errorMessage)) { if (isDefined(errorMessage)) {
@ -36,8 +28,7 @@ export const VerifyEffect = () => {
} }
if (isDefined(loginToken)) { if (isDefined(loginToken)) {
setIsAppWaitingForFreshObjectMetadata(true); verifyLoginToken(loginToken);
getAuthTokensFromLoginToken(loginToken);
} else if (!isLogged) { } else if (!isLogged) {
navigate(AppPath.SignInUp); navigate(AppPath.SignInUp);
} }

View File

@ -0,0 +1,70 @@
import { renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { AppPath } from '@/types/AppPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { useAuth } from '../useAuth';
import { useVerifyLogin } from '../useVerifyLogin';
jest.mock('../useAuth', () => ({
useAuth: jest.fn(),
}));
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar', () => ({
useSnackBar: jest.fn(),
}));
jest.mock('~/hooks/useNavigateApp', () => ({
useNavigateApp: jest.fn(),
}));
const renderHooks = () => {
const { result } = renderHook(() => useVerifyLogin(), {
wrapper: RecoilRoot,
});
return { result };
};
describe('useVerifyLogin', () => {
const mockGetAuthTokensFromLoginToken = jest.fn();
const mockEnqueueSnackBar = jest.fn();
const mockNavigate = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(useAuth as jest.Mock).mockReturnValue({
getAuthTokensFromLoginToken: mockGetAuthTokensFromLoginToken,
});
(useSnackBar as jest.Mock).mockReturnValue({
enqueueSnackBar: mockEnqueueSnackBar,
});
(useNavigateApp as jest.Mock).mockReturnValue(mockNavigate);
});
it('should verify login token', async () => {
const { result } = renderHooks();
await result.current.verifyLoginToken('test-token');
expect(mockGetAuthTokensFromLoginToken).toHaveBeenCalledWith('test-token');
});
it('should handle verification error', async () => {
const error = new Error('Verification failed');
mockGetAuthTokensFromLoginToken.mockRejectedValueOnce(error);
const { result } = renderHooks();
await result.current.verifyLoginToken('test-token');
expect(mockEnqueueSnackBar).toHaveBeenCalledWith('Authentication failed', {
variant: SnackBarVariant.Error,
});
expect(mockNavigate).toHaveBeenCalledWith(AppPath.SignInUp);
});
});

View File

@ -0,0 +1,34 @@
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useAuth } from '@/auth/hooks/useAuth';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { AppPath } from '@/types/AppPath';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useSetRecoilState } from 'recoil';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const useVerifyLogin = () => {
const { enqueueSnackBar } = useSnackBar();
const navigate = useNavigateApp();
const { getAuthTokensFromLoginToken } = useAuth();
const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState(
isAppWaitingForFreshObjectMetadataState,
);
const verifyLoginToken = async (loginToken: string) => {
try {
setIsAppWaitingForFreshObjectMetadata(true);
await getAuthTokensFromLoginToken(loginToken);
} catch (error) {
enqueueSnackBar('Authentication failed', {
variant: SnackBarVariant.Error,
});
navigate(AppPath.SignInUp);
} finally {
setIsAppWaitingForFreshObjectMetadata(false);
}
};
return { verifyLoginToken };
};

View File

@ -27,15 +27,11 @@ import { useUserLookupAdminPanelMutation } from '~/generated/graphql';
import packageJson from '../../../../../package.json'; import packageJson from '../../../../../package.json';
const StyledLinkContainer = styled.div`
margin-right: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledContainer = styled.div` const StyledContainer = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledUserInfo = styled.div` const StyledUserInfo = styled.div`
@ -143,16 +139,14 @@ export const SettingsAdminGeneral = () => {
/> />
<StyledContainer> <StyledContainer>
<StyledLinkContainer> <TextInput
<TextInput value={userIdentifier}
value={userIdentifier} onChange={setUserIdentifier}
onChange={setUserIdentifier} onInputEnter={handleSearch}
onInputEnter={handleSearch} placeholder="Enter user ID or email address"
placeholder="Enter user ID or email address" fullWidth
fullWidth disabled={isUserLookupLoading}
disabled={isUserLookupLoading} />
/>
</StyledLinkContainer>
<Button <Button
Icon={IconSearch} Icon={IconSearch}
variant="primary" variant="primary"

View File

@ -1,6 +1,7 @@
import { Button, H2Title, IconUser, Toggle } from 'twenty-ui'; import { Button, H2Title, IconUser, Toggle } from 'twenty-ui';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState'; import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
import { useFeatureFlagState } from '@/settings/admin-panel/hooks/useFeatureFlagState'; import { useFeatureFlagState } from '@/settings/admin-panel/hooks/useFeatureFlagState';
import { useImpersonationAuth } from '@/settings/admin-panel/hooks/useImpersonationAuth'; import { useImpersonationAuth } from '@/settings/admin-panel/hooks/useImpersonationAuth';
@ -35,19 +36,18 @@ export const SettingsAdminWorkspaceContent = ({
activeWorkspace, activeWorkspace,
}: SettingsAdminWorkspaceContentProps) => { }: SettingsAdminWorkspaceContentProps) => {
const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState); const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState);
const [isImpersonateLoading, setIsImpersonationLoading] = useState(false);
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const [currentUser] = useRecoilState(currentUserState); const [currentUser] = useRecoilState(currentUserState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const [updateFeatureFlag] = useUpdateWorkspaceFeatureFlagMutation();
const [isImpersonateLoading, setIsImpersonationLoading] = useState(false);
const { executeImpersonationAuth } = useImpersonationAuth(); const { executeImpersonationAuth } = useImpersonationAuth();
const { executeImpersonationRedirect } = useImpersonationRedirect(); const { executeImpersonationRedirect } = useImpersonationRedirect();
const [updateFeatureFlag] = useUpdateWorkspaceFeatureFlagMutation();
const [impersonate] = useImpersonateMutation(); const [impersonate] = useImpersonateMutation();
const { updateFeatureFlagState } = useFeatureFlagState(); const { updateFeatureFlagState } = useFeatureFlagState();
const [userLookupResult, setUserLookupResult] = useRecoilState( const userLookupResult = useRecoilValue(userLookupResultState);
userLookupResultState,
);
const handleImpersonate = async (workspaceId: string) => { const handleImpersonate = async (workspaceId: string) => {
if (!userLookupResult?.user.id) { if (!userLookupResult?.user.id) {
@ -63,8 +63,7 @@ export const SettingsAdminWorkspaceContent = ({
variables: { userId: userLookupResult.user.id, workspaceId }, variables: { userId: userLookupResult.user.id, workspaceId },
onCompleted: async (data) => { onCompleted: async (data) => {
const { loginToken, workspace } = data.impersonate; const { loginToken, workspace } = data.impersonate;
const isCurrentWorkspace = workspace.id === activeWorkspace?.id; const isCurrentWorkspace = workspace.id === currentWorkspace?.id;
setUserLookupResult(null);
if (isCurrentWorkspace) { if (isCurrentWorkspace) {
await executeImpersonationAuth(loginToken.token); await executeImpersonationAuth(loginToken.token);
return; return;