Refactor client config (#529)

* Refactor client config

* Fix server tests

* Fix lint
This commit is contained in:
Charles Bochet 2023-07-07 11:10:42 -07:00 committed by GitHub
parent 11d18cc269
commit 26b033abc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 386 additions and 180 deletions

2
front/.gitignore vendored
View File

@ -23,3 +23,5 @@ build-storybook.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.nyc_output

View File

@ -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<UserOrderByWithRelationInput>;
accountOwnerId?: InputMaybe<SortOrderInput>;
accountOwnerId?: InputMaybe<SortOrder>;
address?: InputMaybe<SortOrder>;
createdAt?: InputMaybe<SortOrder>;
domainName?: InputMaybe<SortOrder>;
employees?: InputMaybe<SortOrderInput>;
employees?: InputMaybe<SortOrder>;
id?: InputMaybe<SortOrder>;
name?: InputMaybe<SortOrder>;
people?: InputMaybe<PersonOrderByRelationAggregateInput>;
@ -1247,11 +1256,6 @@ export type NullableStringFieldUpdateOperationsInput = {
set?: InputMaybe<Scalars['String']>;
};
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<SortOrder>;
company?: InputMaybe<CompanyOrderByWithRelationInput>;
companyId?: InputMaybe<SortOrderInput>;
companyId?: InputMaybe<SortOrder>;
createdAt?: InputMaybe<SortOrder>;
email?: InputMaybe<SortOrder>;
firstName?: InputMaybe<SortOrder>;
@ -1567,6 +1571,7 @@ export type PipelineProgressCreateInput = {
export type PipelineProgressCreateManyPipelineInput = {
amount?: InputMaybe<Scalars['Int']>;
closeDate?: InputMaybe<Scalars['DateTime']>;
createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>;
pipelineStageId: Scalars['String'];
@ -1582,6 +1587,7 @@ export type PipelineProgressCreateManyPipelineInputEnvelope = {
export type PipelineProgressCreateManyPipelineStageInput = {
amount?: InputMaybe<Scalars['Int']>;
closeDate?: InputMaybe<Scalars['DateTime']>;
createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>;
pipelineId: Scalars['String'];
@ -1597,6 +1603,7 @@ export type PipelineProgressCreateManyPipelineStageInputEnvelope = {
export type PipelineProgressCreateManyWorkspaceInput = {
amount?: InputMaybe<Scalars['Int']>;
closeDate?: InputMaybe<Scalars['DateTime']>;
createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>;
pipelineId: Scalars['String'];
@ -1642,6 +1649,7 @@ export type PipelineProgressCreateOrConnectWithoutWorkspaceInput = {
export type PipelineProgressCreateWithoutPipelineInput = {
amount?: InputMaybe<Scalars['Int']>;
closeDate?: InputMaybe<Scalars['DateTime']>;
createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>;
pipelineStage: PipelineStageCreateNestedOneWithoutPipelineProgressesInput;
@ -1652,6 +1660,7 @@ export type PipelineProgressCreateWithoutPipelineInput = {
export type PipelineProgressCreateWithoutPipelineStageInput = {
amount?: InputMaybe<Scalars['Int']>;
closeDate?: InputMaybe<Scalars['DateTime']>;
createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>;
pipeline: PipelineCreateNestedOneWithoutPipelineProgressesInput;
@ -1662,6 +1671,7 @@ export type PipelineProgressCreateWithoutPipelineStageInput = {
export type PipelineProgressCreateWithoutWorkspaceInput = {
amount?: InputMaybe<Scalars['Int']>;
closeDate?: InputMaybe<Scalars['DateTime']>;
createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>;
pipeline: PipelineCreateNestedOneWithoutPipelineProgressesInput;
@ -1682,8 +1692,8 @@ export type PipelineProgressOrderByRelationAggregateInput = {
};
export type PipelineProgressOrderByWithRelationInput = {
amount?: InputMaybe<SortOrderInput>;
closeDate?: InputMaybe<SortOrderInput>;
amount?: InputMaybe<SortOrder>;
closeDate?: InputMaybe<SortOrder>;
createdAt?: InputMaybe<SortOrder>;
id?: InputMaybe<SortOrder>;
pipeline?: InputMaybe<PipelineOrderByWithRelationInput>;
@ -1714,6 +1724,7 @@ export type PipelineProgressScalarWhereInput = {
NOT?: InputMaybe<Array<PipelineProgressScalarWhereInput>>;
OR?: InputMaybe<Array<PipelineProgressScalarWhereInput>>;
amount?: InputMaybe<IntNullableFilter>;
closeDate?: InputMaybe<DateTimeNullableFilter>;
createdAt?: InputMaybe<DateTimeFilter>;
id?: InputMaybe<StringFilter>;
pipelineId?: InputMaybe<StringFilter>;
@ -1737,6 +1748,7 @@ export type PipelineProgressUpdateInput = {
export type PipelineProgressUpdateManyMutationInput = {
amount?: InputMaybe<NullableIntFieldUpdateOperationsInput>;
closeDate?: InputMaybe<NullableDateTimeFieldUpdateOperationsInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>;
progressableId?: InputMaybe<StringFieldUpdateOperationsInput>;
@ -1818,6 +1830,7 @@ export type PipelineProgressUpdateWithWhereUniqueWithoutWorkspaceInput = {
export type PipelineProgressUpdateWithoutPipelineInput = {
amount?: InputMaybe<NullableIntFieldUpdateOperationsInput>;
closeDate?: InputMaybe<NullableDateTimeFieldUpdateOperationsInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>;
pipelineStage?: InputMaybe<PipelineStageUpdateOneRequiredWithoutPipelineProgressesNestedInput>;
@ -1828,6 +1841,7 @@ export type PipelineProgressUpdateWithoutPipelineInput = {
export type PipelineProgressUpdateWithoutPipelineStageInput = {
amount?: InputMaybe<NullableIntFieldUpdateOperationsInput>;
closeDate?: InputMaybe<NullableDateTimeFieldUpdateOperationsInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>;
pipeline?: InputMaybe<PipelineUpdateOneRequiredWithoutPipelineProgressesNestedInput>;
@ -1838,6 +1852,7 @@ export type PipelineProgressUpdateWithoutPipelineStageInput = {
export type PipelineProgressUpdateWithoutWorkspaceInput = {
amount?: InputMaybe<NullableIntFieldUpdateOperationsInput>;
closeDate?: InputMaybe<NullableDateTimeFieldUpdateOperationsInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>;
pipeline?: InputMaybe<PipelineUpdateOneRequiredWithoutPipelineProgressesNestedInput>;
@ -2356,11 +2371,6 @@ export enum SortOrder {
Desc = 'desc'
}
export type SortOrderInput = {
nulls?: InputMaybe<NullsOrder>;
sort: SortOrder;
};
export type StringFieldUpdateOperationsInput = {
set?: InputMaybe<Scalars['String']>;
};
@ -2395,6 +2405,12 @@ export type StringNullableFilter = {
startsWith?: InputMaybe<Scalars['String']>;
};
export type Telemetry = {
__typename?: 'Telemetry';
anonymizationEnabled: Scalars['Boolean'];
enabled: Scalars['Boolean'];
};
export type User = {
__typename?: 'User';
avatarUrl?: Maybe<Scalars['String']>;
@ -2476,7 +2492,7 @@ export type UserCreateWithoutWorkspaceMemberInput = {
};
export type UserOrderByWithRelationInput = {
avatarUrl?: InputMaybe<SortOrderInput>;
avatarUrl?: InputMaybe<SortOrder>;
comments?: InputMaybe<CommentOrderByRelationAggregateInput>;
companies?: InputMaybe<CompanyOrderByRelationAggregateInput>;
createdAt?: InputMaybe<SortOrder>;
@ -2486,10 +2502,10 @@ export type UserOrderByWithRelationInput = {
firstName?: InputMaybe<SortOrder>;
id?: InputMaybe<SortOrder>;
lastName?: InputMaybe<SortOrder>;
lastSeen?: InputMaybe<SortOrderInput>;
lastSeen?: InputMaybe<SortOrder>;
locale?: InputMaybe<SortOrder>;
metadata?: InputMaybe<SortOrderInput>;
phoneNumber?: InputMaybe<SortOrderInput>;
metadata?: InputMaybe<SortOrder>;
phoneNumber?: InputMaybe<SortOrder>;
updatedAt?: InputMaybe<SortOrder>;
};
@ -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<Scalars['String']>;
@ -3074,41 +3090,6 @@ export function useCreateEventMutation(baseOptions?: Apollo.MutationHookOptions<
export type CreateEventMutationHookResult = ReturnType<typeof useCreateEventMutation>;
export type CreateEventMutationResult = Apollo.MutationResult<CreateEventMutation>;
export type CreateEventMutationOptions = Apollo.BaseMutationOptions<CreateEventMutation, CreateEventMutationVariables>;
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<GetClientConfigQuery, GetClientConfigQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetClientConfigQuery, GetClientConfigQueryVariables>(GetClientConfigDocument, options);
}
export function useGetClientConfigLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetClientConfigQuery, GetClientConfigQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetClientConfigQuery, GetClientConfigQueryVariables>(GetClientConfigDocument, options);
}
export type GetClientConfigQueryHookResult = ReturnType<typeof useGetClientConfigQuery>;
export type GetClientConfigLazyQueryHookResult = ReturnType<typeof useGetClientConfigLazyQuery>;
export type GetClientConfigQueryResult = Apollo.QueryResult<GetClientConfigQuery, GetClientConfigQueryVariables>;
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<R
export type RenewTokenMutationHookResult = ReturnType<typeof useRenewTokenMutation>;
export type RenewTokenMutationResult = Apollo.MutationResult<RenewTokenMutation>;
export type RenewTokenMutationOptions = Apollo.BaseMutationOptions<RenewTokenMutation, RenewTokenMutationVariables>;
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<GetClientConfigQuery, GetClientConfigQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetClientConfigQuery, GetClientConfigQueryVariables>(GetClientConfigDocument, options);
}
export function useGetClientConfigLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetClientConfigQuery, GetClientConfigQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetClientConfigQuery, GetClientConfigQueryVariables>(GetClientConfigDocument, options);
}
export type GetClientConfigQueryHookResult = ReturnType<typeof useGetClientConfigQuery>;
export type GetClientConfigLazyQueryHookResult = ReturnType<typeof useGetClientConfigLazyQuery>;
export type GetClientConfigQueryResult = Apollo.QueryResult<GetClientConfigQuery, GetClientConfigQueryVariables>;
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
}
}
`;

View File

@ -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(
<AppThemeProvider>
<StrictMode>
<UserProvider>
<BrowserRouter>
<ClientConfigProvider>
<ClientConfigProvider>
<BrowserRouter>
<App />
</ClientConfigProvider>
</BrowserRouter>
</BrowserRouter>
</ClientConfigProvider>
</UserProvider>
</StrictMode>
</AppThemeProvider>

View File

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

View File

@ -1,4 +0,0 @@
export function useIsTelemetryEnabled() {
// TODO: replace by clientConfig
return process.env.IS_TELEMETRY_ENABLED !== 'false';
}

View File

@ -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<ApolloFactory<NormalizedCacheObject> | 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) {

View File

@ -29,6 +29,7 @@ export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
onTokenPairChange?: (tokenPair: AuthTokenPair) => void;
onUnauthenticatedError?: () => void;
extraLinks?: ApolloLink[];
isDebugMode?: boolean;
}
export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
@ -43,6 +44,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
onTokenPairChange,
onUnauthenticatedError,
extraLinks,
isDebugMode,
...options
} = opts;
@ -98,7 +100,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
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<TCacheShape> implements ApolloManager<TCacheShape> {
}
if (networkError) {
if (process.env.NODE_ENV === 'development') {
if (isDebugMode) {
console.warn(`[Network error]: ${networkError}`);
}
onNetworkError?.(networkError);
@ -127,8 +129,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
errorLink,
authLink,
...(extraLinks ? extraLinks : []),
// Only show logger in dev mode
process.env.NODE_ENV !== 'production' ? logger : null,
isDebugMode ? logger : null,
retryLink,
httpLink,
].filter(assertNotNull),

View File

@ -1,2 +1 @@
export * from './select';
export * from './update';

View File

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

View File

@ -1,6 +1,6 @@
import { atom } from 'recoil';
export const authFlowUserEmailState = atom({
export const authFlowUserEmailState = atom<string>({
key: 'authFlowUserEmailState',
default: process.env.NODE_ENV === 'development' ? 'tim@apple.dev' : '',
default: '',
});

View File

@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const displayGoogleLogin = atom<boolean>({
key: 'displayGoogleLogin',
default: true,
});

View File

@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const prefillLoginWithSeed = atom<boolean>({
key: 'prefillLoginWithSeed',
default: true,
});

View File

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

View File

@ -0,0 +1,8 @@
import { atom } from 'recoil';
import { AuthProviders } from '~/generated/graphql';
export const authProvidersState = atom<AuthProviders>({
key: 'authProvidersState',
default: { google: false, magicLink: false, password: true },
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isDebugModeState = atom<boolean>({
key: 'isDebugModeState',
default: false,
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isDemoModeState = atom<boolean>({
key: 'isDemoModeState',
default: false,
});

View File

@ -0,0 +1,8 @@
import { atom } from 'recoil';
import { Telemetry } from '~/generated/graphql';
export const telemetryState = atom<Telemetry>({
key: 'telemetryState',
default: { enabled: true, anonymizationEnabled: true },
});

View File

@ -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() {
</AnimatedEaseIn>
<Title animate>Welcome to Twenty</Title>
<StyledContentContainer>
<MainButton
icon={<IconBrandGoogle size={theme.icon.size.sm} stroke={4} />}
title="Continue with Google"
onClick={onGoogleLoginClick}
fullWidth
/>
{authProviders.google && (
<MainButton
icon={<IconBrandGoogle size={theme.icon.size.sm} stroke={4} />}
title="Continue with Google"
onClick={onGoogleLoginClick}
fullWidth
/>
)}
{visible && (
<motion.div
initial={{ opacity: 0, height: 0 }}

View File

@ -11,6 +11,7 @@ import { Title } from '@/auth/components/ui/Title';
import { useAuth } from '@/auth/hooks/useAuth';
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
import { isMockModeState } from '@/auth/states/isMockModeState';
import { isDemoModeState } from '@/client-config/states/isDemoModeState';
import { MainButton } from '@/ui/components/buttons/MainButton';
import { TextInput } from '@/ui/components/inputs/TextInput';
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
@ -48,15 +49,15 @@ const StyledErrorContainer = styled.div`
export function PasswordLogin() {
const navigate = useNavigate();
const prefillPassword =
process.env.NODE_ENV === 'development' ? 'Applecar2025' : '';
const [isDemoMode] = useRecoilState(isDemoModeState);
const [authFlowUserEmail, setAuthFlowUserEmail] = useRecoilState(
authFlowUserEmailState,
);
const [, setMockMode] = useRecoilState(isMockModeState);
const [internalPassword, setInternalPassword] = useState(prefillPassword);
const [internalPassword, setInternalPassword] = useState(
isDemoMode ? 'Applecar2025' : '',
);
const [formError, setFormError] = useState('');
const { login } = useAuth();

View File

@ -0,0 +1,34 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { useFetchClientConfig } from '@/auth/hooks/useFetchClientConfig';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { isDemoModeState } from '@/client-config/states/isDemoModeState';
import { telemetryState } from '@/client-config/states/telemetryState';
export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
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}</>;
};

View File

@ -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<React.PropsWithChildren> = ({
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}</>;
};

View File

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

View File

@ -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>(AnalyticsResolver);

View File

@ -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>(AnalyticsService);

View File

@ -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,
},

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import { GoogleStrategy } from '../strategies/google.auth.strategy';
export class GoogleProviderEnabledGuard implements CanActivate {
constructor(private readonly environmentService: EnvironmentService) {}
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
if (!this.environmentService.getAuthGoogleEnabled()) {
if (!this.environmentService.isAuthGoogleEnabled()) {
throw new NotFoundException('Google auth is not enabled');
}

View File

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

View File

@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { ClientConfigResolver } from './client-config.resolver';
@Module({
providers: [ClientConfigResolver],
})
export class ClientConfigModule {}

View File

@ -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>(ClientConfigResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

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

View File

@ -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,

View File

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

View File

@ -8,8 +8,22 @@ import { StorageType } from './interfaces/storage.interface';
export class EnvironmentService {
constructor(private configService: ConfigService) {}
getDebugMode(): boolean | undefined {
return this.configService.get<boolean>('DEBUG_MODE')!;
isDebugMode(): boolean {
return this.configService.get<boolean>('DEBUG_MODE') ?? false;
}
isDemoMode(): boolean {
return this.configService.get<boolean>('DEMO_MODE') ?? false;
}
isTelemetryEnabled(): boolean {
return this.configService.get<boolean>('TELEMETRY_ENABLED') ?? true;
}
isTelemetryAnonymizationEnabled(): boolean | undefined {
return (
this.configService.get<boolean>('TELEMETRY_ANONYMIZATION_ENABLED') ?? true
);
}
getPGDatabaseUrl(): string {
@ -44,7 +58,7 @@ export class EnvironmentService {
return this.configService.get<string>('FRONT_AUTH_CALLBACK_URL')!;
}
getAuthGoogleEnabled(): boolean | undefined {
isAuthGoogleEnabled(): boolean | undefined {
return this.configService.get<boolean>('AUTH_GOOGLE_ENABLED');
}

View File

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

View File

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

View File

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