fix: user has to login every time chrome sidepanel is opened (#5544)
We can pass the auth tokens to our front app via post message, which will also allow us to pass route names to navigate on it
This commit is contained in:
@ -13,6 +13,8 @@ import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
|
||||
import { VerifyEffect } from '@/auth/components/VerifyEffect';
|
||||
import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect';
|
||||
import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider';
|
||||
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
|
||||
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
@ -85,36 +87,41 @@ const ProvidersThatNeedRouterContext = () => {
|
||||
const pageTitle = getPageTitleFromPath(pathname);
|
||||
|
||||
return (
|
||||
<ApolloProvider>
|
||||
<ClientConfigProviderEffect />
|
||||
<ClientConfigProvider>
|
||||
<UserProviderEffect />
|
||||
<UserProvider>
|
||||
<ApolloMetadataClientProvider>
|
||||
<ObjectMetadataItemsProvider>
|
||||
<PrefetchDataProvider>
|
||||
<AppThemeProvider>
|
||||
<SnackBarProvider>
|
||||
<DialogManagerScope dialogManagerScopeId="dialog-manager">
|
||||
<DialogManager>
|
||||
<StrictMode>
|
||||
<PromiseRejectionEffect />
|
||||
<CommandMenuEffect />
|
||||
<GotoHotkeysEffect />
|
||||
<PageTitle title={pageTitle} />
|
||||
<Outlet />
|
||||
</StrictMode>
|
||||
</DialogManager>
|
||||
</DialogManagerScope>
|
||||
</SnackBarProvider>
|
||||
</AppThemeProvider>
|
||||
</PrefetchDataProvider>
|
||||
<PageChangeEffect />
|
||||
</ObjectMetadataItemsProvider>
|
||||
</ApolloMetadataClientProvider>
|
||||
</UserProvider>
|
||||
</ClientConfigProvider>
|
||||
</ApolloProvider>
|
||||
<>
|
||||
<ApolloProvider>
|
||||
<ClientConfigProviderEffect />
|
||||
<ClientConfigProvider>
|
||||
<ChromeExtensionSidecarEffect />
|
||||
<ChromeExtensionSidecarProvider>
|
||||
<UserProviderEffect />
|
||||
<UserProvider>
|
||||
<ApolloMetadataClientProvider>
|
||||
<ObjectMetadataItemsProvider>
|
||||
<PrefetchDataProvider>
|
||||
<AppThemeProvider>
|
||||
<SnackBarProvider>
|
||||
<DialogManagerScope dialogManagerScopeId="dialog-manager">
|
||||
<DialogManager>
|
||||
<StrictMode>
|
||||
<PromiseRejectionEffect />
|
||||
<CommandMenuEffect />
|
||||
<GotoHotkeysEffect />
|
||||
<PageTitle title={pageTitle} />
|
||||
<Outlet />
|
||||
</StrictMode>
|
||||
</DialogManager>
|
||||
</DialogManagerScope>
|
||||
</SnackBarProvider>
|
||||
</AppThemeProvider>
|
||||
</PrefetchDataProvider>
|
||||
<PageChangeEffect />
|
||||
</ObjectMetadataItemsProvider>
|
||||
</ApolloMetadataClientProvider>
|
||||
</UserProvider>
|
||||
</ChromeExtensionSidecarProvider>
|
||||
</ClientConfigProvider>
|
||||
</ApolloProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -133,6 +133,7 @@ export type ClientConfig = {
|
||||
authProviders: AuthProviders;
|
||||
billing: Billing;
|
||||
captcha: Captcha;
|
||||
chromeExtensionId?: Maybe<Scalars['String']>;
|
||||
debugMode: Scalars['Boolean'];
|
||||
sentry: Sentry;
|
||||
signInPrefilled: Scalars['Boolean'];
|
||||
@ -1186,7 +1187,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat
|
||||
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null } } };
|
||||
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null } } };
|
||||
|
||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, currentCacheVersion?: string | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: string, interval?: string | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
|
||||
|
||||
@ -2307,6 +2308,7 @@ export const GetClientConfigDocument = gql`
|
||||
provider
|
||||
siteKey
|
||||
}
|
||||
chromeExtensionId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { isLoadingTokensFromExtensionState } from '@/chrome-extension-sidecar/states/isLoadingTokensFromExtensionState';
|
||||
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isInFrame } from '~/utils/isInIframe';
|
||||
|
||||
export const ChromeExtensionSidecarEffect = () => {
|
||||
const navigate = useNavigate();
|
||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||
const chromeExtensionId = useRecoilValue(chromeExtensionIdState);
|
||||
const setIsLoadingTokensFromExtension = useSetRecoilState(
|
||||
isLoadingTokensFromExtensionState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInFrame() && isDefined(chromeExtensionId)) {
|
||||
window.parent.postMessage(
|
||||
'loaded',
|
||||
`chrome-extension://${chromeExtensionId}`,
|
||||
);
|
||||
|
||||
const handleWindowEvents = (event: MessageEvent<any>) => {
|
||||
if (event.origin === `chrome-extension://${chromeExtensionId}`) {
|
||||
switch (event.data.type) {
|
||||
case 'tokens': {
|
||||
setTokenPair(event.data.value);
|
||||
setIsLoadingTokensFromExtension(true);
|
||||
break;
|
||||
}
|
||||
case 'navigate':
|
||||
navigate(event.data.value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
setIsLoadingTokensFromExtension(false);
|
||||
return;
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', handleWindowEvents);
|
||||
return () => {
|
||||
window.removeEventListener('message', handleWindowEvents);
|
||||
};
|
||||
}
|
||||
}, [
|
||||
chromeExtensionId,
|
||||
setIsLoadingTokensFromExtension,
|
||||
setTokenPair,
|
||||
navigate,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { isLoadingTokensFromExtensionState } from '@/chrome-extension-sidecar/states/isLoadingTokensFromExtensionState';
|
||||
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isInFrame } from '~/utils/isInIframe';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const AppInaccessible = ({ message }: { message: string }) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<img
|
||||
src="/images/integrations/twenty-logo.svg"
|
||||
alt="twenty-icon"
|
||||
height={40}
|
||||
width={40}
|
||||
/>
|
||||
<h3>{message}</h3>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChromeExtensionSidecarProvider: React.FC<
|
||||
React.PropsWithChildren
|
||||
> = ({ children }) => {
|
||||
const isLoadingTokensFromExtension = useRecoilValue(
|
||||
isLoadingTokensFromExtensionState,
|
||||
);
|
||||
const chromeExtensionId = useRecoilValue(chromeExtensionIdState);
|
||||
|
||||
if (!isInFrame()) return <>{children}</>;
|
||||
|
||||
if (!isDefined(chromeExtensionId))
|
||||
return (
|
||||
<AppInaccessible message={`Twenty is not accessible inside an iframe.`} />
|
||||
);
|
||||
|
||||
if (isDefined(isLoadingTokensFromExtension) && !isLoadingTokensFromExtension)
|
||||
return (
|
||||
<AppInaccessible
|
||||
message={`Unauthorized access from iframe origin. If you're trying to access from chrome extension,
|
||||
please check your chrome extension ID on your server.
|
||||
`}
|
||||
/>
|
||||
);
|
||||
|
||||
return isLoadingTokensFromExtension && <>{children}</>;
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const isLoadingTokensFromExtensionState = createState<boolean | null>({
|
||||
key: 'isLoadingTokensFromExtensionState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@ -4,6 +4,7 @@ import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
|
||||
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
|
||||
import { isClientConfigLoadedState } from '@/client-config/states/isClientConfigLoadedState';
|
||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
||||
@ -32,6 +33,8 @@ export const ClientConfigProviderEffect = () => {
|
||||
|
||||
const setCaptchaProvider = useSetRecoilState(captchaProviderState);
|
||||
|
||||
const setChromeExtensionId = useSetRecoilState(chromeExtensionIdState);
|
||||
|
||||
const { data, loading } = useGetClientConfigQuery({
|
||||
skip: isClientConfigLoaded,
|
||||
});
|
||||
@ -63,6 +66,8 @@ export const ClientConfigProviderEffect = () => {
|
||||
provider: data?.clientConfig?.captcha?.provider,
|
||||
siteKey: data?.clientConfig?.captcha?.siteKey,
|
||||
});
|
||||
|
||||
setChromeExtensionId(data?.clientConfig?.chromeExtensionId);
|
||||
}
|
||||
}, [
|
||||
data,
|
||||
@ -77,6 +82,7 @@ export const ClientConfigProviderEffect = () => {
|
||||
loading,
|
||||
setIsClientConfigLoaded,
|
||||
setCaptchaProvider,
|
||||
setChromeExtensionId,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
|
||||
@ -33,6 +33,7 @@ export const GET_CLIENT_CONFIG = gql`
|
||||
provider
|
||||
siteKey
|
||||
}
|
||||
chromeExtensionId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const chromeExtensionIdState = createState<string | null | undefined>({
|
||||
key: 'chromeExtensionIdState',
|
||||
defaultValue: null,
|
||||
});
|
||||
7
packages/twenty-front/src/utils/isInIframe.ts
Normal file
7
packages/twenty-front/src/utils/isInIframe.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const isInFrame = () => {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user