From 26b033abc93bfc891953905f6bff2f8b189df614 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Fri, 7 Jul 2023 11:10:42 -0700 Subject: [PATCH] Refactor client config (#529) * Refactor client config * Fix server tests * Fix lint --- front/.gitignore | 2 + front/src/generated/graphql.tsx | 150 ++++++++++-------- front/src/index.tsx | 10 +- .../analytics/hooks/useEventTracker.ts | 10 +- .../analytics/hooks/useIsTelemetryEnabled.ts | 4 - .../modules/apollo/hooks/useApolloFactory.ts | 5 +- .../modules/apollo/services/apollo.factory.ts | 9 +- front/src/modules/auth/services/index.ts | 1 - front/src/modules/auth/services/select.ts | 10 -- .../auth/states/authFlowUserEmailState.ts | 4 +- .../modules/auth/states/displayGoogleLogin.ts | 6 - .../auth/states/prefillLoginWithSeed.ts | 6 - .../modules/client-config/queries/index.tsx | 18 +++ .../states/authProvidersState.ts | 8 + .../client-config/states/isDebugModeState.ts | 6 + .../client-config/states/isDemoModeState.ts | 6 + .../client-config/states/telemetryState.ts | 8 + front/src/pages/auth/Index.tsx | 26 ++- front/src/pages/auth/PasswordLogin.tsx | 9 +- .../client-config/ClientConfigProvider.tsx | 34 ++++ .../clientConfig/ClientConfigProvider.tsx | 21 --- server/.env.example | 1 - .../core/analytics/analytics.resolver.spec.ts | 10 +- .../core/analytics/analytics.service.spec.ts | 9 +- .../src/core/analytics/analytics.service.ts | 22 ++- server/src/core/auth/auth.resolver.ts | 15 +- server/src/core/auth/dto/token.entity.ts | 9 -- .../guards/google-provider-enabled.guard.ts | 2 +- .../client-config/client-config.entity.ts | 37 +++++ .../client-config/client-config.module.ts | 7 + .../client-config.resolver.spec.ts | 25 +++ .../client-config/client-config.resolver.ts | 28 ++++ server/src/core/core.module.ts | 2 + server/src/database/prisma.service.ts | 2 +- .../environment/environment.service.ts | 20 ++- .../environment/environment.validation.ts | 17 +- .../src/integrations/integrations.module.ts | 2 +- server/src/utils/anonymize.ts | 5 +- 38 files changed, 386 insertions(+), 180 deletions(-) delete mode 100644 front/src/modules/analytics/hooks/useIsTelemetryEnabled.ts delete mode 100644 front/src/modules/auth/services/select.ts delete mode 100644 front/src/modules/auth/states/displayGoogleLogin.ts delete mode 100644 front/src/modules/auth/states/prefillLoginWithSeed.ts create mode 100644 front/src/modules/client-config/queries/index.tsx create mode 100644 front/src/modules/client-config/states/authProvidersState.ts create mode 100644 front/src/modules/client-config/states/isDebugModeState.ts create mode 100644 front/src/modules/client-config/states/isDemoModeState.ts create mode 100644 front/src/modules/client-config/states/telemetryState.ts create mode 100644 front/src/providers/client-config/ClientConfigProvider.tsx delete mode 100644 front/src/providers/clientConfig/ClientConfigProvider.tsx create mode 100644 server/src/core/client-config/client-config.entity.ts create mode 100644 server/src/core/client-config/client-config.module.ts create mode 100644 server/src/core/client-config/client-config.resolver.spec.ts create mode 100644 server/src/core/client-config/client-config.resolver.ts diff --git a/front/.gitignore b/front/.gitignore index 310daec59c..ea617b5d6e 100644 --- a/front/.gitignore +++ b/front/.gitignore @@ -23,3 +23,5 @@ build-storybook.log npm-debug.log* yarn-debug.log* yarn-error.log* + +.nyc_output diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 013c9a606c..2e22b453b8 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -29,6 +29,13 @@ export type Analytics = { success: Scalars['Boolean']; }; +export type AuthProviders = { + __typename?: 'AuthProviders'; + google: Scalars['Boolean']; + magicLink: Scalars['Boolean']; + password: Scalars['Boolean']; +}; + export type AuthToken = { __typename?: 'AuthToken'; expiresAt: Scalars['DateTime']; @@ -57,8 +64,10 @@ export type BoolFilter = { export type ClientConfig = { __typename?: 'ClientConfig'; - display_google_login: Scalars['Boolean']; - prefill_login_with_seed: Scalars['Boolean']; + authProviders: AuthProviders; + debugMode: Scalars['Boolean']; + demoMode: Scalars['Boolean']; + telemetry: Telemetry; }; export type Comment = { @@ -756,11 +765,11 @@ export type CompanyOrderByRelationAggregateInput = { export type CompanyOrderByWithRelationInput = { accountOwner?: InputMaybe; - accountOwnerId?: InputMaybe; + accountOwnerId?: InputMaybe; address?: InputMaybe; createdAt?: InputMaybe; domainName?: InputMaybe; - employees?: InputMaybe; + employees?: InputMaybe; id?: InputMaybe; name?: InputMaybe; people?: InputMaybe; @@ -1247,11 +1256,6 @@ export type NullableStringFieldUpdateOperationsInput = { set?: InputMaybe; }; -export enum NullsOrder { - First = 'first', - Last = 'last' -} - export type Person = { __typename?: 'Person'; _commentCount: Scalars['Int']; @@ -1332,7 +1336,7 @@ export type PersonOrderByRelationAggregateInput = { export type PersonOrderByWithRelationInput = { city?: InputMaybe; company?: InputMaybe; - companyId?: InputMaybe; + companyId?: InputMaybe; createdAt?: InputMaybe; email?: InputMaybe; firstName?: InputMaybe; @@ -1567,6 +1571,7 @@ export type PipelineProgressCreateInput = { export type PipelineProgressCreateManyPipelineInput = { amount?: InputMaybe; + closeDate?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; pipelineStageId: Scalars['String']; @@ -1582,6 +1587,7 @@ export type PipelineProgressCreateManyPipelineInputEnvelope = { export type PipelineProgressCreateManyPipelineStageInput = { amount?: InputMaybe; + closeDate?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; pipelineId: Scalars['String']; @@ -1597,6 +1603,7 @@ export type PipelineProgressCreateManyPipelineStageInputEnvelope = { export type PipelineProgressCreateManyWorkspaceInput = { amount?: InputMaybe; + closeDate?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; pipelineId: Scalars['String']; @@ -1642,6 +1649,7 @@ export type PipelineProgressCreateOrConnectWithoutWorkspaceInput = { export type PipelineProgressCreateWithoutPipelineInput = { amount?: InputMaybe; + closeDate?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; pipelineStage: PipelineStageCreateNestedOneWithoutPipelineProgressesInput; @@ -1652,6 +1660,7 @@ export type PipelineProgressCreateWithoutPipelineInput = { export type PipelineProgressCreateWithoutPipelineStageInput = { amount?: InputMaybe; + closeDate?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; pipeline: PipelineCreateNestedOneWithoutPipelineProgressesInput; @@ -1662,6 +1671,7 @@ export type PipelineProgressCreateWithoutPipelineStageInput = { export type PipelineProgressCreateWithoutWorkspaceInput = { amount?: InputMaybe; + closeDate?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; pipeline: PipelineCreateNestedOneWithoutPipelineProgressesInput; @@ -1682,8 +1692,8 @@ export type PipelineProgressOrderByRelationAggregateInput = { }; export type PipelineProgressOrderByWithRelationInput = { - amount?: InputMaybe; - closeDate?: InputMaybe; + amount?: InputMaybe; + closeDate?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; pipeline?: InputMaybe; @@ -1714,6 +1724,7 @@ export type PipelineProgressScalarWhereInput = { NOT?: InputMaybe>; OR?: InputMaybe>; amount?: InputMaybe; + closeDate?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; pipelineId?: InputMaybe; @@ -1737,6 +1748,7 @@ export type PipelineProgressUpdateInput = { export type PipelineProgressUpdateManyMutationInput = { amount?: InputMaybe; + closeDate?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; progressableId?: InputMaybe; @@ -1818,6 +1830,7 @@ export type PipelineProgressUpdateWithWhereUniqueWithoutWorkspaceInput = { export type PipelineProgressUpdateWithoutPipelineInput = { amount?: InputMaybe; + closeDate?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; pipelineStage?: InputMaybe; @@ -1828,6 +1841,7 @@ export type PipelineProgressUpdateWithoutPipelineInput = { export type PipelineProgressUpdateWithoutPipelineStageInput = { amount?: InputMaybe; + closeDate?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; pipeline?: InputMaybe; @@ -1838,6 +1852,7 @@ export type PipelineProgressUpdateWithoutPipelineStageInput = { export type PipelineProgressUpdateWithoutWorkspaceInput = { amount?: InputMaybe; + closeDate?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; pipeline?: InputMaybe; @@ -2356,11 +2371,6 @@ export enum SortOrder { Desc = 'desc' } -export type SortOrderInput = { - nulls?: InputMaybe; - sort: SortOrder; -}; - export type StringFieldUpdateOperationsInput = { set?: InputMaybe; }; @@ -2395,6 +2405,12 @@ export type StringNullableFilter = { startsWith?: InputMaybe; }; +export type Telemetry = { + __typename?: 'Telemetry'; + anonymizationEnabled: Scalars['Boolean']; + enabled: Scalars['Boolean']; +}; + export type User = { __typename?: 'User'; avatarUrl?: Maybe; @@ -2476,7 +2492,7 @@ export type UserCreateWithoutWorkspaceMemberInput = { }; export type UserOrderByWithRelationInput = { - avatarUrl?: InputMaybe; + avatarUrl?: InputMaybe; comments?: InputMaybe; companies?: InputMaybe; createdAt?: InputMaybe; @@ -2486,10 +2502,10 @@ export type UserOrderByWithRelationInput = { firstName?: InputMaybe; id?: InputMaybe; lastName?: InputMaybe; - lastSeen?: InputMaybe; + lastSeen?: InputMaybe; locale?: InputMaybe; - metadata?: InputMaybe; - phoneNumber?: InputMaybe; + metadata?: InputMaybe; + phoneNumber?: InputMaybe; updatedAt?: InputMaybe; }; @@ -2766,11 +2782,6 @@ export type CreateEventMutationVariables = Exact<{ export type CreateEventMutation = { __typename?: 'Mutation', createEvent: { __typename?: 'Analytics', success: boolean } }; -export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; - - -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', display_google_login: boolean, prefill_login_with_seed: boolean } }; - export type ChallengeMutationVariables = Exact<{ email: Scalars['String']; password: Scalars['String']; @@ -2793,6 +2804,11 @@ export type RenewTokenMutationVariables = Exact<{ export type RenewTokenMutation = { __typename?: 'Mutation', renewToken: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', expiresAt: string, token: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', demoMode: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean } } }; + export type CreateCommentMutationVariables = Exact<{ commentId: Scalars['String']; commentText: Scalars['String']; @@ -2954,7 +2970,7 @@ export type UpdateOnePipelineProgressMutationVariables = Exact<{ }>; -export type UpdateOnePipelineProgressMutation = { __typename?: 'Mutation', updateOnePipelineProgress?: { __typename?: 'PipelineProgress', id: string } | null }; +export type UpdateOnePipelineProgressMutation = { __typename?: 'Mutation', updateOnePipelineProgress?: { __typename?: 'PipelineProgress', id: string, amount?: number | null, closeDate?: string | null } | null }; export type UpdateOnePipelineProgressStageMutationVariables = Exact<{ id?: InputMaybe; @@ -3074,41 +3090,6 @@ export function useCreateEventMutation(baseOptions?: Apollo.MutationHookOptions< export type CreateEventMutationHookResult = ReturnType; export type CreateEventMutationResult = Apollo.MutationResult; export type CreateEventMutationOptions = Apollo.BaseMutationOptions; -export const GetClientConfigDocument = gql` - query GetClientConfig { - clientConfig { - display_google_login - prefill_login_with_seed - } -} - `; - -/** - * __useGetClientConfigQuery__ - * - * To run a query within a React component, call `useGetClientConfigQuery` and pass it any options that fit your needs. - * When your component renders, `useGetClientConfigQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useGetClientConfigQuery({ - * variables: { - * }, - * }); - */ -export function useGetClientConfigQuery(baseOptions?: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(GetClientConfigDocument, options); - } -export function useGetClientConfigLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(GetClientConfigDocument, options); - } -export type GetClientConfigQueryHookResult = ReturnType; -export type GetClientConfigLazyQueryHookResult = ReturnType; -export type GetClientConfigQueryResult = Apollo.QueryResult; export const ChallengeDocument = gql` mutation Challenge($email: String!, $password: String!) { challenge(email: $email, password: $password) { @@ -3246,6 +3227,49 @@ export function useRenewTokenMutation(baseOptions?: Apollo.MutationHookOptions; export type RenewTokenMutationResult = Apollo.MutationResult; export type RenewTokenMutationOptions = Apollo.BaseMutationOptions; +export const GetClientConfigDocument = gql` + query GetClientConfig { + clientConfig { + authProviders { + google + password + } + demoMode + debugMode + telemetry { + enabled + anonymizationEnabled + } + } +} + `; + +/** + * __useGetClientConfigQuery__ + * + * To run a query within a React component, call `useGetClientConfigQuery` and pass it any options that fit your needs. + * When your component renders, `useGetClientConfigQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetClientConfigQuery({ + * variables: { + * }, + * }); + */ +export function useGetClientConfigQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetClientConfigDocument, options); + } +export function useGetClientConfigLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetClientConfigDocument, options); + } +export type GetClientConfigQueryHookResult = ReturnType; +export type GetClientConfigLazyQueryHookResult = ReturnType; +export type GetClientConfigQueryResult = Apollo.QueryResult; export const CreateCommentDocument = gql` mutation CreateComment($commentId: String!, $commentText: String!, $authorId: String!, $commentThreadId: String!, $createdAt: DateTime!) { createOneComment( @@ -4016,6 +4040,8 @@ export const UpdateOnePipelineProgressDocument = gql` data: {amount: {set: $amount}, closeDate: {set: $closeDate}} ) { id + amount + closeDate } } `; diff --git a/front/src/index.tsx b/front/src/index.tsx index 9948b45af2..6345cc7a2a 100644 --- a/front/src/index.tsx +++ b/front/src/index.tsx @@ -8,7 +8,7 @@ import { ThemeType } from '@/ui/themes/themes'; import '@emotion/react'; import { ApolloProvider } from './providers/apollo/ApolloProvider'; -import { ClientConfigProvider } from './providers/clientConfig/ClientConfigProvider'; +import { ClientConfigProvider } from './providers/client-config/ClientConfigProvider'; import { AppThemeProvider } from './providers/theme/AppThemeProvider'; import { UserProvider } from './providers/user/UserProvider'; import { App } from './App'; @@ -26,11 +26,11 @@ root.render( - - + + - - + + diff --git a/front/src/modules/analytics/hooks/useEventTracker.ts b/front/src/modules/analytics/hooks/useEventTracker.ts index b3b50a3cde..acad4ba30d 100644 --- a/front/src/modules/analytics/hooks/useEventTracker.ts +++ b/front/src/modules/analytics/hooks/useEventTracker.ts @@ -1,9 +1,9 @@ import { useCallback } from 'react'; +import { useRecoilState } from 'recoil'; +import { telemetryState } from '@/client-config/states/telemetryState'; import { useCreateEventMutation } from '~/generated/graphql'; -import { useIsTelemetryEnabled } from './useIsTelemetryEnabled'; - interface EventLocation { pathname: string; } @@ -13,12 +13,12 @@ export interface EventData { } export function useEventTracker() { - const telemetryEnabled = useIsTelemetryEnabled(); + const [telemetry] = useRecoilState(telemetryState); const [createEventMutation] = useCreateEventMutation(); return useCallback( (eventType: string, eventData: EventData) => { - if (telemetryEnabled) { + if (telemetry.enabled) { createEventMutation({ variables: { type: eventType, @@ -27,6 +27,6 @@ export function useEventTracker() { }); } }, - [createEventMutation, telemetryEnabled], + [createEventMutation, telemetry], ); } diff --git a/front/src/modules/analytics/hooks/useIsTelemetryEnabled.ts b/front/src/modules/analytics/hooks/useIsTelemetryEnabled.ts deleted file mode 100644 index 9c053f572b..0000000000 --- a/front/src/modules/analytics/hooks/useIsTelemetryEnabled.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function useIsTelemetryEnabled() { - // TODO: replace by clientConfig - return process.env.IS_TELEMETRY_ENABLED !== 'false'; -} diff --git a/front/src/modules/apollo/hooks/useApolloFactory.ts b/front/src/modules/apollo/hooks/useApolloFactory.ts index f1670f0519..840f70cb39 100644 --- a/front/src/modules/apollo/hooks/useApolloFactory.ts +++ b/front/src/modules/apollo/hooks/useApolloFactory.ts @@ -8,6 +8,7 @@ import { useRecoilState } from 'recoil'; import { isMockModeState } from '@/auth/states/isMockModeState'; import { tokenPairState } from '@/auth/states/tokenPairState'; +import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { CommentThreadTarget } from '~/generated/graphql'; import { mockedCompaniesData } from '~/testing/mock-data/companies'; import { mockedUsersData } from '~/testing/mock-data/users'; @@ -16,6 +17,7 @@ import { ApolloFactory } from '../services/apollo.factory'; export function useApolloFactory() { const apolloRef = useRef | null>(null); + const [isDebugMode] = useRecoilState(isDebugModeState); const [tokenPair, setTokenPair] = useRecoilState(tokenPairState); const [isMockMode] = useRecoilState(isMockModeState); @@ -64,10 +66,11 @@ export function useApolloFactory() { setTokenPair(null); }, extraLinks: isMockMode ? [mockLink] : [], + isDebugMode, }); return apolloRef.current.getClient(); - }, [isMockMode, setTokenPair]); + }, [isMockMode, setTokenPair, isDebugMode]); useEffect(() => { if (apolloRef.current) { diff --git a/front/src/modules/apollo/services/apollo.factory.ts b/front/src/modules/apollo/services/apollo.factory.ts index 3ff9a8bfcd..34553e8e83 100644 --- a/front/src/modules/apollo/services/apollo.factory.ts +++ b/front/src/modules/apollo/services/apollo.factory.ts @@ -29,6 +29,7 @@ export interface Options extends ApolloClientOptions { onTokenPairChange?: (tokenPair: AuthTokenPair) => void; onUnauthenticatedError?: () => void; extraLinks?: ApolloLink[]; + isDebugMode?: boolean; } export class ApolloFactory implements ApolloManager { @@ -43,6 +44,7 @@ export class ApolloFactory implements ApolloManager { onTokenPairChange, onUnauthenticatedError, extraLinks, + isDebugMode, ...options } = opts; @@ -98,7 +100,7 @@ export class ApolloFactory implements ApolloManager { return forward(operation); } default: - if (process.env.NODE_ENV === 'development') { + if (isDebugMode) { console.warn( `[GraphQL error]: Message: ${ graphQLError.message @@ -114,7 +116,7 @@ export class ApolloFactory implements ApolloManager { } if (networkError) { - if (process.env.NODE_ENV === 'development') { + if (isDebugMode) { console.warn(`[Network error]: ${networkError}`); } onNetworkError?.(networkError); @@ -127,8 +129,7 @@ export class ApolloFactory implements ApolloManager { errorLink, authLink, ...(extraLinks ? extraLinks : []), - // Only show logger in dev mode - process.env.NODE_ENV !== 'production' ? logger : null, + isDebugMode ? logger : null, retryLink, httpLink, ].filter(assertNotNull), diff --git a/front/src/modules/auth/services/index.ts b/front/src/modules/auth/services/index.ts index 18c6c2f7dd..c37c258c7c 100644 --- a/front/src/modules/auth/services/index.ts +++ b/front/src/modules/auth/services/index.ts @@ -1,2 +1 @@ -export * from './select'; export * from './update'; diff --git a/front/src/modules/auth/services/select.ts b/front/src/modules/auth/services/select.ts deleted file mode 100644 index 2386bd4485..0000000000 --- a/front/src/modules/auth/services/select.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { gql } from '@apollo/client'; - -export const GET_CLIENT_CONFIG = gql` - query GetClientConfig { - clientConfig { - display_google_login - prefill_login_with_seed - } - } -`; diff --git a/front/src/modules/auth/states/authFlowUserEmailState.ts b/front/src/modules/auth/states/authFlowUserEmailState.ts index 0224bd3b06..ca3d1d42bf 100644 --- a/front/src/modules/auth/states/authFlowUserEmailState.ts +++ b/front/src/modules/auth/states/authFlowUserEmailState.ts @@ -1,6 +1,6 @@ import { atom } from 'recoil'; -export const authFlowUserEmailState = atom({ +export const authFlowUserEmailState = atom({ key: 'authFlowUserEmailState', - default: process.env.NODE_ENV === 'development' ? 'tim@apple.dev' : '', + default: '', }); diff --git a/front/src/modules/auth/states/displayGoogleLogin.ts b/front/src/modules/auth/states/displayGoogleLogin.ts deleted file mode 100644 index 465582c6aa..0000000000 --- a/front/src/modules/auth/states/displayGoogleLogin.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { atom } from 'recoil'; - -export const displayGoogleLogin = atom({ - key: 'displayGoogleLogin', - default: true, -}); diff --git a/front/src/modules/auth/states/prefillLoginWithSeed.ts b/front/src/modules/auth/states/prefillLoginWithSeed.ts deleted file mode 100644 index c174bfd0c7..0000000000 --- a/front/src/modules/auth/states/prefillLoginWithSeed.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { atom } from 'recoil'; - -export const prefillLoginWithSeed = atom({ - key: 'prefillLoginWithSeed', - default: true, -}); diff --git a/front/src/modules/client-config/queries/index.tsx b/front/src/modules/client-config/queries/index.tsx new file mode 100644 index 0000000000..cd227da930 --- /dev/null +++ b/front/src/modules/client-config/queries/index.tsx @@ -0,0 +1,18 @@ +import { gql } from '@apollo/client'; + +export const GET_CLIENT_CONFIG = gql` + query GetClientConfig { + clientConfig { + authProviders { + google + password + } + demoMode + debugMode + telemetry { + enabled + anonymizationEnabled + } + } + } +`; diff --git a/front/src/modules/client-config/states/authProvidersState.ts b/front/src/modules/client-config/states/authProvidersState.ts new file mode 100644 index 0000000000..e0b087fa40 --- /dev/null +++ b/front/src/modules/client-config/states/authProvidersState.ts @@ -0,0 +1,8 @@ +import { atom } from 'recoil'; + +import { AuthProviders } from '~/generated/graphql'; + +export const authProvidersState = atom({ + key: 'authProvidersState', + default: { google: false, magicLink: false, password: true }, +}); diff --git a/front/src/modules/client-config/states/isDebugModeState.ts b/front/src/modules/client-config/states/isDebugModeState.ts new file mode 100644 index 0000000000..82d8be49d1 --- /dev/null +++ b/front/src/modules/client-config/states/isDebugModeState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const isDebugModeState = atom({ + key: 'isDebugModeState', + default: false, +}); diff --git a/front/src/modules/client-config/states/isDemoModeState.ts b/front/src/modules/client-config/states/isDemoModeState.ts new file mode 100644 index 0000000000..753ed61cef --- /dev/null +++ b/front/src/modules/client-config/states/isDemoModeState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const isDemoModeState = atom({ + key: 'isDemoModeState', + default: false, +}); diff --git a/front/src/modules/client-config/states/telemetryState.ts b/front/src/modules/client-config/states/telemetryState.ts new file mode 100644 index 0000000000..c6d0958079 --- /dev/null +++ b/front/src/modules/client-config/states/telemetryState.ts @@ -0,0 +1,8 @@ +import { atom } from 'recoil'; + +import { Telemetry } from '~/generated/graphql'; + +export const telemetryState = atom({ + key: 'telemetryState', + default: { enabled: true, anonymizationEnabled: true }, +}); diff --git a/front/src/pages/auth/Index.tsx b/front/src/pages/auth/Index.tsx index d6b9000840..bf14d2fe57 100644 --- a/front/src/pages/auth/Index.tsx +++ b/front/src/pages/auth/Index.tsx @@ -12,6 +12,7 @@ import { Logo } from '@/auth/components/ui/Logo'; import { Title } from '@/auth/components/ui/Title'; import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState'; import { isMockModeState } from '@/auth/states/isMockModeState'; +import { authProvidersState } from '@/client-config/states/authProvidersState'; import { captureHotkeyTypeInFocusState } from '@/hotkeys/states/captureHotkeyTypeInFocusState'; import { MainButton } from '@/ui/components/buttons/MainButton'; import { TextInput } from '@/ui/components/inputs/TextInput'; @@ -36,6 +37,8 @@ export function Index() { const navigate = useNavigate(); const theme = useTheme(); const [, setMockMode] = useRecoilState(isMockModeState); + const [authProviders] = useRecoilState(authProvidersState); + const [demoMode] = useRecoilState(authProvidersState); const [authFlowUserEmail, setAuthFlowUserEmail] = useRecoilState( authFlowUserEmailState, @@ -71,7 +74,14 @@ export function Index() { useEffect(() => { setMockMode(true); setCaptureHotkeyTypeInFocus(true); - }, [navigate, setMockMode, setCaptureHotkeyTypeInFocus]); + setAuthFlowUserEmail(demoMode ? 'tim@apple.dev' : ''); + }, [ + navigate, + setMockMode, + setCaptureHotkeyTypeInFocus, + setAuthFlowUserEmail, + demoMode, + ]); return ( <> @@ -80,12 +90,14 @@ export function Index() { Welcome to Twenty - } - title="Continue with Google" - onClick={onGoogleLoginClick} - fullWidth - /> + {authProviders.google && ( + } + title="Continue with Google" + onClick={onGoogleLoginClick} + fullWidth + /> + )} {visible && ( = ({ + children, +}) => { + const [, setAuthProviders] = useRecoilState(authProvidersState); + const [, setDebugMode] = useRecoilState(isDebugModeState); + const [, setDemoMode] = useRecoilState(isDemoModeState); + const [, setTelemetry] = useRecoilState(telemetryState); + + const clientConfig = useFetchClientConfig(); + + useEffect(() => { + if (clientConfig) { + setAuthProviders({ + google: clientConfig.authProviders.google, + password: clientConfig.authProviders.password, + magicLink: false, + }); + setDebugMode(clientConfig.debugMode); + setDemoMode(clientConfig.demoMode); + setTelemetry(clientConfig.telemetry); + } + }, [clientConfig, setAuthProviders, setDebugMode, setDemoMode, setTelemetry]); + + return <>{children}; +}; diff --git a/front/src/providers/clientConfig/ClientConfigProvider.tsx b/front/src/providers/clientConfig/ClientConfigProvider.tsx deleted file mode 100644 index a141b0a31b..0000000000 --- a/front/src/providers/clientConfig/ClientConfigProvider.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useEffect } from 'react'; -import { useRecoilState } from 'recoil'; - -import { useFetchClientConfig } from '@/auth/hooks/useFetchClientConfig'; -import { displayGoogleLogin } from '@/auth/states/displayGoogleLogin'; -import { prefillLoginWithSeed } from '@/auth/states/prefillLoginWithSeed'; - -export const ClientConfigProvider: React.FC = ({ - children, -}) => { - const [, setDisplayGoogleLogin] = useRecoilState(displayGoogleLogin); - const [, setPrefillLoginWithSeed] = useRecoilState(prefillLoginWithSeed); - const clientConfig = useFetchClientConfig(); - - useEffect(() => { - setDisplayGoogleLogin(clientConfig?.display_google_login ?? true); - setPrefillLoginWithSeed(clientConfig?.prefill_login_with_seed ?? true); - }, [setDisplayGoogleLogin, setPrefillLoginWithSeed, clientConfig]); - - return <>{children}; -}; diff --git a/server/.env.example b/server/.env.example index 855f680054..080d8ec59a 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,5 +1,4 @@ DEBUG_MODE=false -AUTH_GOOGLE_ENABLED=false ACCESS_TOKEN_SECRET=secret_jwt ACCESS_TOKEN_EXPIRES_IN=5m LOGIN_TOKEN_SECRET=secret_login_token diff --git a/server/src/core/analytics/analytics.resolver.spec.ts b/server/src/core/analytics/analytics.resolver.spec.ts index 5bf52c7680..efd0272d0c 100644 --- a/server/src/core/analytics/analytics.resolver.spec.ts +++ b/server/src/core/analytics/analytics.resolver.spec.ts @@ -1,13 +1,21 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AnalyticsResolver } from './analytics.resolver'; import { AnalyticsService } from './analytics.service'; +import { EnvironmentService } from 'src/integrations/environment/environment.service'; describe('AnalyticsResolver', () => { let resolver: AnalyticsResolver; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [AnalyticsResolver, AnalyticsService], + providers: [ + AnalyticsResolver, + AnalyticsService, + { + provide: EnvironmentService, + useValue: {}, + }, + ], }).compile(); resolver = module.get(AnalyticsResolver); diff --git a/server/src/core/analytics/analytics.service.spec.ts b/server/src/core/analytics/analytics.service.spec.ts index 9abc31062a..554f3cba6b 100644 --- a/server/src/core/analytics/analytics.service.spec.ts +++ b/server/src/core/analytics/analytics.service.spec.ts @@ -1,12 +1,19 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AnalyticsService } from './analytics.service'; +import { EnvironmentService } from 'src/integrations/environment/environment.service'; describe('AnalyticsService', () => { let service: AnalyticsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [AnalyticsService], + providers: [ + AnalyticsService, + { + provide: EnvironmentService, + useValue: {}, + }, + ], }).compile(); service = module.get(AnalyticsService); diff --git a/server/src/core/analytics/analytics.service.ts b/server/src/core/analytics/analytics.service.ts index 932666a4ec..b778eaa60f 100644 --- a/server/src/core/analytics/analytics.service.ts +++ b/server/src/core/analytics/analytics.service.ts @@ -3,12 +3,13 @@ import { User, Workspace } from '@prisma/client'; import axios, { AxiosInstance } from 'axios'; import { CreateAnalyticsInput } from './dto/create-analytics.input'; import { anonymize } from 'src/utils/anonymize'; +import { EnvironmentService } from 'src/integrations/environment/environment.service'; @Injectable() export class AnalyticsService { private readonly httpService: AxiosInstance; - constructor() { + constructor(private readonly environmentService: EnvironmentService) { this.httpService = axios.create({ baseURL: 'https://t.twenty.com/api/v1/s2s', }); @@ -19,15 +20,26 @@ export class AnalyticsService { user: User | undefined, workspace: Workspace | undefined, ) { - if (process.env.IS_TELEMETRY_ENABLED === 'false') { - return; + if (!this.environmentService.isTelemetryEnabled()) { + return { success: true }; } + const anonymizationEnabled = + this.environmentService.isTelemetryAnonymizationEnabled(); + const data = { type: createEventInput.type, data: { - userUUID: user ? anonymize(user.id) : undefined, - workspaceUUID: workspace ? anonymize(workspace.id) : undefined, + userUUID: user + ? anonymizationEnabled + ? anonymize(user.id) + : user.id + : undefined, + workspaceUUID: workspace + ? anonymizationEnabled + ? anonymize(workspace.id) + : workspace.id + : undefined, workspaceDomain: workspace ? workspace.domainName : undefined, ...createEventInput.data, }, diff --git a/server/src/core/auth/auth.resolver.ts b/server/src/core/auth/auth.resolver.ts index a09c2ef462..d84dde440b 100644 --- a/server/src/core/auth/auth.resolver.ts +++ b/server/src/core/auth/auth.resolver.ts @@ -1,5 +1,5 @@ import { Args, Mutation, Resolver, Query } from '@nestjs/graphql'; -import { AuthTokens, ClientConfig } from './dto/token.entity'; +import { AuthTokens } from './dto/token.entity'; import { TokenService } from './services/token.service'; import { RefreshTokenInput } from './dto/refresh-token.input'; import { BadRequestException } from '@nestjs/common'; @@ -61,17 +61,4 @@ export class AuthResolver { return { tokens: tokens }; } - - @Query(() => ClientConfig) - async clientConfig(): Promise { - const displayGoogleLogin = process.env.AUTH_GOOGLE_CLIENT_ID !== undefined; - const prefillLoginWithSeed = process.env.NODE_ENV === 'development'; - - const clientConfig: ClientConfig = { - display_google_login: displayGoogleLogin, - prefill_login_with_seed: prefillLoginWithSeed, - }; - - return Promise.resolve(clientConfig); - } } diff --git a/server/src/core/auth/dto/token.entity.ts b/server/src/core/auth/dto/token.entity.ts index 2f7156e1c6..a1b8b6f7af 100644 --- a/server/src/core/auth/dto/token.entity.ts +++ b/server/src/core/auth/dto/token.entity.ts @@ -23,12 +23,3 @@ export class AuthTokens { @Field(() => AuthTokenPair) tokens: AuthTokenPair; } - -@ObjectType() -export class ClientConfig { - @Field(() => Boolean) - display_google_login: boolean; - - @Field(() => Boolean) - prefill_login_with_seed: boolean; -} diff --git a/server/src/core/auth/guards/google-provider-enabled.guard.ts b/server/src/core/auth/guards/google-provider-enabled.guard.ts index 75eb412b5f..5dd4365bba 100644 --- a/server/src/core/auth/guards/google-provider-enabled.guard.ts +++ b/server/src/core/auth/guards/google-provider-enabled.guard.ts @@ -7,7 +7,7 @@ import { GoogleStrategy } from '../strategies/google.auth.strategy'; export class GoogleProviderEnabledGuard implements CanActivate { constructor(private readonly environmentService: EnvironmentService) {} canActivate(): boolean | Promise | Observable { - if (!this.environmentService.getAuthGoogleEnabled()) { + if (!this.environmentService.isAuthGoogleEnabled()) { throw new NotFoundException('Google auth is not enabled'); } diff --git a/server/src/core/client-config/client-config.entity.ts b/server/src/core/client-config/client-config.entity.ts new file mode 100644 index 0000000000..d8405dce2b --- /dev/null +++ b/server/src/core/client-config/client-config.entity.ts @@ -0,0 +1,37 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +class AuthProviders { + @Field(() => Boolean) + google: boolean; + + @Field(() => Boolean) + magicLink: boolean; + + @Field(() => Boolean) + password: boolean; +} + +@ObjectType() +class Telemetry { + @Field(() => Boolean) + enabled: boolean; + + @Field(() => Boolean) + anonymizationEnabled: boolean; +} + +@ObjectType() +export class ClientConfig { + @Field(() => AuthProviders, { nullable: false }) + authProviders: AuthProviders; + + @Field(() => Telemetry, { nullable: false }) + telemetry: Telemetry; + + @Field(() => Boolean) + demoMode: boolean; + + @Field(() => Boolean) + debugMode: boolean; +} diff --git a/server/src/core/client-config/client-config.module.ts b/server/src/core/client-config/client-config.module.ts new file mode 100644 index 0000000000..6cc2058564 --- /dev/null +++ b/server/src/core/client-config/client-config.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { ClientConfigResolver } from './client-config.resolver'; + +@Module({ + providers: [ClientConfigResolver], +}) +export class ClientConfigModule {} diff --git a/server/src/core/client-config/client-config.resolver.spec.ts b/server/src/core/client-config/client-config.resolver.spec.ts new file mode 100644 index 0000000000..3d26757d8b --- /dev/null +++ b/server/src/core/client-config/client-config.resolver.spec.ts @@ -0,0 +1,25 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ClientConfigResolver } from './client-config.resolver'; +import { EnvironmentService } from 'src/integrations/environment/environment.service'; + +describe('ClientConfigResolver', () => { + let resolver: ClientConfigResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ClientConfigResolver, + { + provide: EnvironmentService, + useValue: {}, + }, + ], + }).compile(); + + resolver = module.get(ClientConfigResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/server/src/core/client-config/client-config.resolver.ts b/server/src/core/client-config/client-config.resolver.ts new file mode 100644 index 0000000000..66b11014dd --- /dev/null +++ b/server/src/core/client-config/client-config.resolver.ts @@ -0,0 +1,28 @@ +import { Resolver, Query } from '@nestjs/graphql'; +import { EnvironmentService } from 'src/integrations/environment/environment.service'; +import { ClientConfig } from './client-config.entity'; + +@Resolver() +export class ClientConfigResolver { + constructor(private environmentService: EnvironmentService) {} + + @Query(() => ClientConfig) + async clientConfig(): Promise { + const clientConfig: ClientConfig = { + authProviders: { + google: this.environmentService.isAuthGoogleEnabled() ?? false, + magicLink: false, + password: true, + }, + telemetry: { + enabled: this.environmentService.isTelemetryEnabled() ?? false, + anonymizationEnabled: + this.environmentService.isTelemetryAnonymizationEnabled() ?? false, + }, + demoMode: this.environmentService.isDemoMode() ?? false, + debugMode: this.environmentService.isDebugMode() ?? false, + }; + + return Promise.resolve(clientConfig); + } +} diff --git a/server/src/core/core.module.ts b/server/src/core/core.module.ts index 16b4b031c4..cccfed5d08 100644 --- a/server/src/core/core.module.ts +++ b/server/src/core/core.module.ts @@ -8,6 +8,7 @@ import { AuthModule } from './auth/auth.module'; import { WorkspaceModule } from './workspace/workspace.module'; import { AnalyticsModule } from './analytics/analytics.module'; import { FileModule } from './file/file.module'; +import { ClientConfigModule } from './client-config/client-config.module'; @Module({ imports: [ @@ -20,6 +21,7 @@ import { FileModule } from './file/file.module'; WorkspaceModule, AnalyticsModule, FileModule, + ClientConfigModule, ], exports: [ AuthModule, diff --git a/server/src/database/prisma.service.ts b/server/src/database/prisma.service.ts index 401f75736f..9d6b306989 100644 --- a/server/src/database/prisma.service.ts +++ b/server/src/database/prisma.service.ts @@ -19,7 +19,7 @@ export class PrismaService extends PrismaClient implements OnModuleInit { private readonly logger = new Logger(PrismaService.name); constructor(private readonly environmentService: EnvironmentService) { - const debugMode = environmentService.getDebugMode(); + const debugMode = environmentService.isDebugMode(); super({ errorFormat: 'minimal', log: debugMode diff --git a/server/src/integrations/environment/environment.service.ts b/server/src/integrations/environment/environment.service.ts index 0e4f84003d..84a72e1ba3 100644 --- a/server/src/integrations/environment/environment.service.ts +++ b/server/src/integrations/environment/environment.service.ts @@ -8,8 +8,22 @@ import { StorageType } from './interfaces/storage.interface'; export class EnvironmentService { constructor(private configService: ConfigService) {} - getDebugMode(): boolean | undefined { - return this.configService.get('DEBUG_MODE')!; + isDebugMode(): boolean { + return this.configService.get('DEBUG_MODE') ?? false; + } + + isDemoMode(): boolean { + return this.configService.get('DEMO_MODE') ?? false; + } + + isTelemetryEnabled(): boolean { + return this.configService.get('TELEMETRY_ENABLED') ?? true; + } + + isTelemetryAnonymizationEnabled(): boolean | undefined { + return ( + this.configService.get('TELEMETRY_ANONYMIZATION_ENABLED') ?? true + ); } getPGDatabaseUrl(): string { @@ -44,7 +58,7 @@ export class EnvironmentService { return this.configService.get('FRONT_AUTH_CALLBACK_URL')!; } - getAuthGoogleEnabled(): boolean | undefined { + isAuthGoogleEnabled(): boolean | undefined { return this.configService.get('AUTH_GOOGLE_ENABLED'); } diff --git a/server/src/integrations/environment/environment.validation.ts b/server/src/integrations/environment/environment.validation.ts index 18740e499f..a461533d6d 100644 --- a/server/src/integrations/environment/environment.validation.ts +++ b/server/src/integrations/environment/environment.validation.ts @@ -16,12 +16,27 @@ import { IsAWSRegion } from './decorators/is-aws-region.decorator'; import { CastToBoolean } from './decorators/cast-to-boolean.decorator'; export class EnvironmentVariables { - // Stage + // Misc @CastToBoolean() @IsOptional() @IsBoolean() DEBUG_MODE?: boolean; + @CastToBoolean() + @IsOptional() + @IsBoolean() + DEMO_MODE?: boolean; + + @CastToBoolean() + @IsOptional() + @IsBoolean() + TELEMETRY_ENABLED?: boolean; + + @CastToBoolean() + @IsOptional() + @IsBoolean() + TELEMETRY_ANONYMIZATION_ENABLED?: boolean; + // Database @IsUrl({ protocols: ['postgres'], require_tld: false }) PG_DATABASE_URL: string; diff --git a/server/src/integrations/integrations.module.ts b/server/src/integrations/integrations.module.ts index 30809754e0..d6b2604356 100644 --- a/server/src/integrations/integrations.module.ts +++ b/server/src/integrations/integrations.module.ts @@ -1,4 +1,4 @@ -import { Global, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; import { EnvironmentModule } from './environment/environment.module'; import { EnvironmentService } from './environment/environment.service'; diff --git a/server/src/utils/anonymize.ts b/server/src/utils/anonymize.ts index 5b4f7b335f..3393a1f681 100644 --- a/server/src/utils/anonymize.ts +++ b/server/src/utils/anonymize.ts @@ -1,9 +1,6 @@ import crypto from 'crypto'; -export function anonymize(input) { - if (process.env.IS_TELEMETRY_ANONYMIZATION_ENABLED === 'false') { - return input; - } +export function anonymize(input: string) { // md5 shorter than sha-256 and collisions are not a security risk in this use-case return crypto.createHash('md5').update(input).digest('hex'); }