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:
Aditya Pimpalkar
2024-05-30 11:58:45 +01:00
committed by GitHub
parent d770e56e31
commit a12c1aad5e
30 changed files with 511 additions and 231 deletions

View File

@ -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>
</>
);
};

View File

@ -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
}
}
`;

View File

@ -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 <></>;
};

View File

@ -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}</>;
};

View File

@ -0,0 +1,6 @@
import { createState } from 'twenty-ui';
export const isLoadingTokensFromExtensionState = createState<boolean | null>({
key: 'isLoadingTokensFromExtensionState',
defaultValue: null,
});

View File

@ -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 <></>;

View File

@ -33,6 +33,7 @@ export const GET_CLIENT_CONFIG = gql`
provider
siteKey
}
chromeExtensionId
}
}
`;

View File

@ -0,0 +1,6 @@
import { createState } from 'twenty-ui';
export const chromeExtensionIdState = createState<string | null | undefined>({
key: 'chromeExtensionIdState',
defaultValue: null,
});

View File

@ -0,0 +1,7 @@
export const isInFrame = () => {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
};