diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index ecc79a5e5a35..953c6b9e6b3f 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -171,6 +171,7 @@ export type ClientConfig = { api: ApiConfig; authProviders: AuthProviders; billing: Billing; + canManageFeatureFlags: Scalars['Boolean']; captcha: Captcha; chromeExtensionId?: Maybe; debugMode: Scalars['Boolean']; @@ -2081,7 +2082,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isSSOEnabled: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, 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 }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isSSOEnabled: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, canManageFeatureFlags: boolean, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, 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 }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>; @@ -3514,6 +3515,7 @@ export const GetClientConfigDocument = gql` mutationMaximumAffectedRecords } chromeExtensionId + canManageFeatureFlags } } `; diff --git a/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx index 6530532fae8f..a6230e9ff206 100644 --- a/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx +++ b/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx @@ -27,22 +27,17 @@ export const VerifyEffect = () => { ); useEffect(() => { - const getTokens = async () => { - if (isDefined(errorMessage)) { - enqueueSnackBar(errorMessage, { - variant: SnackBarVariant.Error, - }); - } - if (!loginToken) { - navigate(AppPath.SignInUp); - } else { - setIsAppWaitingForFreshObjectMetadata(true); - await verify(loginToken); - } - }; - - if (!isLogged) { - getTokens(); + if (isDefined(errorMessage)) { + enqueueSnackBar(errorMessage, { + variant: SnackBarVariant.Error, + }); + } + + if (isDefined(loginToken)) { + setIsAppWaitingForFreshObjectMetadata(true); + verify(loginToken); + } else if (!isLogged) { + navigate(AppPath.SignInUp); } // Verify only needs to run once at mount // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx index 6430246e359f..99bea99254f8 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx +++ b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx @@ -141,10 +141,7 @@ describe('useAuth', () => { const { result } = renderHooks(); await act(async () => { - const res = await result.current.signUpWithCredentials(email, password); - expect(res).toHaveProperty('user'); - expect(res).toHaveProperty('workspaceMember'); - expect(res).toHaveProperty('workspace'); + await result.current.signUpWithCredentials(email, password); }); expect(mocks[2].result).toHaveBeenCalled(); diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index d28d49d59e06..d583e815d8eb 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -268,6 +268,8 @@ export const useAuth = () => { const handleVerify = useCallback( async (loginToken: string) => { + setIsVerifyPendingState(true); + const verifyResult = await verify({ variables: { loginToken }, }); @@ -282,16 +284,11 @@ export const useAuth = () => { setTokenPair(verifyResult.data?.verify.tokens); - const { user, workspaceMember, workspace } = await loadCurrentUser(); + await loadCurrentUser(); - return { - user, - workspaceMember, - workspace, - tokens: verifyResult.data?.verify.tokens, - }; + setIsVerifyPendingState(false); }, - [verify, setTokenPair, loadCurrentUser], + [setIsVerifyPendingState, verify, setTokenPair, loadCurrentUser], ); const handleCrendentialsSignIn = useCallback( @@ -301,21 +298,9 @@ export const useAuth = () => { password, captchaToken, ); - setIsVerifyPendingState(true); - - const { user, workspaceMember, workspace } = await handleVerify( - loginToken.token, - ); - - setIsVerifyPendingState(false); - - return { - user, - workspaceMember, - workspace, - }; + await handleVerify(loginToken.token); }, - [handleChallenge, handleVerify, setIsVerifyPendingState], + [handleChallenge, handleVerify], ); const handleSignOut = useCallback(async () => { @@ -360,13 +345,7 @@ export const useAuth = () => { ); } - const { user, workspace, workspaceMember } = await handleVerify( - signUpResult.data?.signUp.loginToken.token, - ); - - setIsVerifyPendingState(false); - - return { user, workspaceMember, workspace }; + await handleVerify(signUpResult.data?.signUp.loginToken.token); }, [ setIsVerifyPendingState, diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx index ebbfa965ead7..224812287dd3 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx @@ -1,6 +1,7 @@ import { apiConfigState } from '@/client-config/states/apiConfigState'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { billingState } from '@/client-config/states/billingState'; +import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState'; import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState'; import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState'; @@ -45,6 +46,10 @@ export const ClientConfigProviderEffect = () => { const setApiConfig = useSetRecoilState(apiConfigState); + const setCanManageFeatureFlags = useSetRecoilState( + canManageFeatureFlagsState, + ); + const { data, loading, error } = useGetClientConfigQuery({ skip: clientConfigApiStatus.isLoaded, }); @@ -107,6 +112,7 @@ export const ClientConfigProviderEffect = () => { defaultSubdomain: data?.clientConfig?.defaultSubdomain, frontDomain: data?.clientConfig?.frontDomain, }); + setCanManageFeatureFlags(data?.clientConfig?.canManageFeatureFlags); }, [ data, setIsDebugMode, @@ -125,6 +131,7 @@ export const ClientConfigProviderEffect = () => { setDomainConfiguration, setIsSSOEnabledState, setAuthProviders, + setCanManageFeatureFlags, ]); return <>; diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts index 88a696368988..57aeb22389c4 100644 --- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts @@ -44,6 +44,7 @@ export const GET_CLIENT_CONFIG = gql` mutationMaximumAffectedRecords } chromeExtensionId + canManageFeatureFlags } } `; diff --git a/packages/twenty-front/src/modules/client-config/states/canManageFeatureFlagsState.ts b/packages/twenty-front/src/modules/client-config/states/canManageFeatureFlagsState.ts new file mode 100644 index 000000000000..1d0222a6ea9d --- /dev/null +++ b/packages/twenty-front/src/modules/client-config/states/canManageFeatureFlagsState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const canManageFeatureFlagsState = createState({ + key: 'canManageFeatureFlagsState', + defaultValue: false, +}); diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx index 7dcdb1cf18b8..484932e73bd4 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx @@ -1,5 +1,7 @@ +import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState'; import { SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID } from '@/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs'; import { useFeatureFlagsManagement } from '@/settings/admin-panel/hooks/useFeatureFlagsManagement'; +import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate'; import { TextInput } from '@/ui/input/components/TextInput'; import { TabList } from '@/ui/layout/tab/components/TabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; @@ -11,6 +13,7 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/consta import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards'; import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; import { getImageAbsoluteURI } from 'twenty-shared'; import { Button, @@ -24,7 +27,6 @@ import { Toggle, } from 'twenty-ui'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; -import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate'; const StyledLinkContainer = styled.div` margin-right: ${({ theme }) => theme.spacing(2)}; @@ -47,7 +49,7 @@ const StyledUserInfo = styled.div` `; const StyledTable = styled(Table)` - margin-top: ${({ theme }) => theme.spacing(0.5)}; + margin-top: ${({ theme }) => theme.spacing(3)}; `; const StyledTabListContainer = styled.div` @@ -87,6 +89,8 @@ export const SettingsAdminContent = () => { error, } = useFeatureFlagsManagement(); + const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState); + const handleSearch = async () => { setActiveTabId(''); @@ -151,37 +155,39 @@ export const SettingsAdminContent = () => { /> )} - - - Feature Flag - Status - - - {activeWorkspace.featureFlags.map((flag) => ( + {canManageFeatureFlags && ( + - {flag.key} - - - handleFeatureFlagUpdate( - activeWorkspace.id, - flag.key, - newValue, - ) - } - /> - + Feature Flag + Status - ))} - + + {activeWorkspace.featureFlags.map((flag) => ( + + {flag.key} + + + handleFeatureFlagUpdate( + activeWorkspace.id, + flag.key, + newValue, + ) + } + /> + + + ))} + + )} ); }; @@ -190,8 +196,16 @@ export const SettingsAdminContent = () => { <>
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts index 8c08f657d9b3..326bcfeeccd9 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts +++ b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts @@ -1,15 +1,24 @@ import { currentUserState } from '@/auth/states/currentUserState'; import { AppPath } from '@/types/AppPath'; import { useState } from 'react'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { useImpersonateMutation } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; +import { useAuth } from '@/auth/hooks/useAuth'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState'; export const useImpersonate = () => { const [currentUser] = useRecoilState(currentUserState); - const [impersonate] = useImpersonateMutation(); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState( + isAppWaitingForFreshObjectMetadataState, + ); + + const { verify } = useAuth(); + const [impersonate] = useImpersonateMutation(); const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); const [isLoading, setIsLoading] = useState(false); @@ -39,6 +48,13 @@ export const useImpersonate = () => { const { loginToken, workspace } = impersonateResult.data.impersonate; + if (workspace.id === currentWorkspace?.id) { + setIsAppWaitingForFreshObjectMetadata(true); + await verify(loginToken.token); + setIsAppWaitingForFreshObjectMetadata(false); + return; + } + return redirectToWorkspaceDomain(workspace.subdomain, AppPath.Verify, { loginToken: loginToken.token, }); diff --git a/packages/twenty-front/src/testing/mock-data/config.ts b/packages/twenty-front/src/testing/mock-data/config.ts index a80303d9a5e6..97a62869a11c 100644 --- a/packages/twenty-front/src/testing/mock-data/config.ts +++ b/packages/twenty-front/src/testing/mock-data/config.ts @@ -40,4 +40,5 @@ export const mockedClientConfig: ClientConfig = { __typename: 'Captcha', }, api: { mutationMaximumAffectedRecords: 100 }, + canManageFeatureFlags: true, }; diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts index 16ac678aaccd..6e90eef747c9 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts @@ -3,15 +3,15 @@ import { Args, Mutation, Resolver } from '@nestjs/graphql'; import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service'; import { ImpersonateInput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.input'; +import { ImpersonateOutput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.output'; import { UpdateWorkspaceFeatureFlagInput } from 'src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input'; import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity'; import { UserLookupInput } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.input'; import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { ImpersonateGuard } from 'src/engine/guards/impersonate-guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { ImpersonateGuard } from 'src/engine/guards/impersonate-guard'; -import { ImpersonateOutput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.output'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; @Resolver() @UseFilters(AuthGraphqlApiExceptionFilter) diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts index 0d34a6462479..e45a5afda826 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts @@ -8,14 +8,14 @@ import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; +import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { featureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/feature-flag.validate'; import { User } from 'src/engine/core-modules/user/user.entity'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { userValidator } from 'src/engine/core-modules/user/user.validate'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; -import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; -import { featureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/feature-flag.validate'; @Injectable() export class AdminPanelService { diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts index c94b2418e050..6e3842f5c820 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts @@ -94,4 +94,7 @@ export class ClientConfig { @Field(() => ApiConfig) api: ApiConfig; + + @Field(() => Boolean) + canManageFeatureFlags: boolean; } diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts index cbd1fb7d0181..bc64032070aa 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts @@ -59,6 +59,9 @@ export class ClientConfigResolver { ), }, analyticsEnabled: this.environmentService.get('ANALYTICS_ENABLED'), + canManageFeatureFlags: + this.environmentService.get('DEBUG_MODE') || + this.environmentService.get('IS_BILLING_ENABLED'), }; return Promise.resolve(clientConfig);