impersonate regression fix (#10306)
This commit is contained in:
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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 };
|
||||||
|
};
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user