From c6ae480856deda1c92c6a7536af1ca6988c97523 Mon Sep 17 00:00:00 2001 From: Arthur EICHELBERGER Date: Thu, 11 Jan 2024 11:48:14 +0100 Subject: [PATCH] feat(signup): allow to block signup (#3209) * feat(signup): allow to block signup * feat(signup): update environment variable documentation * test: update auth service tests * feat(signup): prevent user from reaching out the sign up page * Fix lint * Fixes --------- Co-authored-by: Charles Bochet --- .../docs/start/self-hosting/environment-variables.mdx | 1 + .../src/effect-components/PageChangeEffect.tsx | 7 +++++++ packages/twenty-front/src/generated/graphql.tsx | 4 +++- .../client-config/components/ClientConfigProvider.tsx | 4 ++++ .../client-config/graphql/queries/getClientConfig.ts | 1 + .../modules/client-config/states/isSignUpDisabledState.ts | 6 ++++++ packages/twenty-front/src/testing/mock-data/config.ts | 1 + packages/twenty-server/.env.example | 1 + .../src/core/auth/services/auth.service.spec.ts | 5 +++++ .../twenty-server/src/core/auth/services/auth.service.ts | 8 ++++++++ .../src/core/client-config/client-config.entity.ts | 3 +++ .../src/core/client-config/client-config.resolver.ts | 1 + .../src/filters/utils/global-exception-handler.util.ts | 2 ++ .../src/filters/utils/graphql-errors.util.ts | 8 ++++++++ .../src/integrations/environment/environment.service.ts | 4 ++++ .../integrations/environment/environment.validation.ts | 5 +++++ 16 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 packages/twenty-front/src/modules/client-config/states/isSignUpDisabledState.ts diff --git a/packages/twenty-docs/docs/start/self-hosting/environment-variables.mdx b/packages/twenty-docs/docs/start/self-hosting/environment-variables.mdx index c4717a755..db0a927e7 100644 --- a/packages/twenty-docs/docs/start/self-hosting/environment-variables.mdx +++ b/packages/twenty-docs/docs/start/self-hosting/environment-variables.mdx @@ -56,6 +56,7 @@ import TabItem from '@theme/TabItem'; ['AUTH_GOOGLE_CLIENT_SECRET', '', 'Google client secret'], ['AUTH_GOOGLE_CALLBACK_URL', '', 'Google auth callback'], ['FRONT_AUTH_CALLBACK_URL', 'http://localhost:3001/verify ', 'Callback used for Login page'], + ['IS_SIGN_UP_DISABLED', 'false', 'Disable sign-up'], ]}> ### Email diff --git a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx index dc001dcce..c555e9213 100644 --- a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx @@ -1,10 +1,12 @@ import { useEffect, useState } from 'react'; import { matchPath, useLocation, useNavigate } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { useEventTracker } from '@/analytics/hooks/useEventTracker'; import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus'; import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus'; +import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { CommandType } from '@/command-menu/types/Command'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; @@ -41,6 +43,8 @@ export const PageChangeEffect = () => { const openCreateActivity = useOpenCreateActivityDrawer(); + const isSignUpDisabled = useRecoilValue(isSignUpDisabledState); + useEffect(() => { if (!previousLocation || previousLocation !== location.pathname) { setPreviousLocation(location.pathname); @@ -115,10 +119,13 @@ export const PageChangeEffect = () => { navigateToSignUp(); }, }); + } else if (isMatchingLocation(AppPath.SignUp) && isSignUpDisabled) { + navigate(AppPath.SignIn); } }, [ enqueueSnackBar, isMatchingLocation, + isSignUpDisabled, location.pathname, navigate, onboardingStatus, diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 66a687f78..ed10d96ec 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -72,6 +72,7 @@ export type ClientConfig = { debugMode: Scalars['Boolean']; sentry: Sentry; signInPrefilled: Scalars['Boolean']; + signUpDisabled: Scalars['Boolean']; support: Support; telemetry: Telemetry; }; @@ -746,7 +747,7 @@ export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __ export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl: string }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn: string } } }; +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl: string }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn: string } } }; export type UploadFileMutationVariables = Exact<{ file: Scalars['Upload']; @@ -1281,6 +1282,7 @@ export const GetClientConfigDocument = gql` billingUrl } signInPrefilled + signUpDisabled debugMode telemetry { enabled diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx index 2498637b2..f665c2b4e 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx @@ -5,6 +5,7 @@ import { authProvidersState } from '@/client-config/states/authProvidersState'; import { billingState } from '@/client-config/states/billingState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; +import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState'; import { sentryConfigState } from '@/client-config/states/sentryConfigState'; import { supportChatState } from '@/client-config/states/supportChatState'; import { telemetryState } from '@/client-config/states/telemetryState'; @@ -17,6 +18,7 @@ export const ClientConfigProvider: React.FC = ({ const setIsDebugMode = useSetRecoilState(isDebugModeState); const setIsSignInPrefilled = useSetRecoilState(isSignInPrefilledState); + const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState); const setBilling = useSetRecoilState(billingState); const setTelemetry = useSetRecoilState(telemetryState); @@ -35,6 +37,7 @@ export const ClientConfigProvider: React.FC = ({ }); setIsDebugMode(data?.clientConfig.debugMode); setIsSignInPrefilled(data?.clientConfig.signInPrefilled); + setIsSignUpDisabled(data?.clientConfig.signUpDisabled); setBilling(data?.clientConfig.billing); setTelemetry(data?.clientConfig.telemetry); @@ -49,6 +52,7 @@ export const ClientConfigProvider: React.FC = ({ setAuthProviders, setIsDebugMode, setIsSignInPrefilled, + setIsSignUpDisabled, setTelemetry, setSupportChat, setBilling, 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 1637975f5..3f7eb573c 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 @@ -12,6 +12,7 @@ export const GET_CLIENT_CONFIG = gql` billingUrl } signInPrefilled + signUpDisabled debugMode telemetry { enabled diff --git a/packages/twenty-front/src/modules/client-config/states/isSignUpDisabledState.ts b/packages/twenty-front/src/modules/client-config/states/isSignUpDisabledState.ts new file mode 100644 index 000000000..da7ec103e --- /dev/null +++ b/packages/twenty-front/src/modules/client-config/states/isSignUpDisabledState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const isSignUpDisabledState = atom({ + key: 'isSignUpDisabledState', + default: false, +}); diff --git a/packages/twenty-front/src/testing/mock-data/config.ts b/packages/twenty-front/src/testing/mock-data/config.ts index 281c2502f..16504ac90 100644 --- a/packages/twenty-front/src/testing/mock-data/config.ts +++ b/packages/twenty-front/src/testing/mock-data/config.ts @@ -1,5 +1,6 @@ export const mockedClientConfig = { signInPrefilled: true, + signUpDisabled: false, dataModelSettingsEnabled: true, developersSettingsEnabled: true, debugMode: false, diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example index 804b7dccd..be10708fd 100644 --- a/packages/twenty-server/.env.example +++ b/packages/twenty-server/.env.example @@ -21,6 +21,7 @@ SIGN_IN_PREFILLED=true # MESSAGING_PROVIDER_GMAIL_ENABLED=false # IS_BILLING_ENABLED=false # BILLING_PLAN_REQUIRED_LINK=https://twenty.com/stripe-redirection +# IS_SIGN_UP_DISABLED=false # AUTH_GOOGLE_CLIENT_ID=replace_me_with_google_client_id # AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret # AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect diff --git a/packages/twenty-server/src/core/auth/services/auth.service.spec.ts b/packages/twenty-server/src/core/auth/services/auth.service.spec.ts index 6aad675e8..eba5c39cb 100644 --- a/packages/twenty-server/src/core/auth/services/auth.service.spec.ts +++ b/packages/twenty-server/src/core/auth/services/auth.service.spec.ts @@ -7,6 +7,7 @@ import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspa import { FileUploadService } from 'src/core/file/services/file-upload.service'; import { Workspace } from 'src/core/workspace/workspace.entity'; import { User } from 'src/core/user/user.entity'; +import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { AuthService } from './auth.service'; import { TokenService } from './token.service'; @@ -46,6 +47,10 @@ describe('AuthService', () => { provide: getRepositoryToken(User, 'core'), useValue: {}, }, + { + provide: EnvironmentService, + useValue: {}, + }, ], }).compile(); diff --git a/packages/twenty-server/src/core/auth/services/auth.service.ts b/packages/twenty-server/src/core/auth/services/auth.service.ts index 01875c8d1..1c1bf80ee 100644 --- a/packages/twenty-server/src/core/auth/services/auth.service.ts +++ b/packages/twenty-server/src/core/auth/services/auth.service.ts @@ -29,6 +29,7 @@ import { UserService } from 'src/core/user/services/user.service'; import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service'; import { getImageBufferFromUrl } from 'src/utils/image'; import { FileUploadService } from 'src/core/file/services/file-upload.service'; +import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { TokenService } from './token.service'; @@ -50,6 +51,7 @@ export class AuthService { @InjectRepository(User, 'core') private readonly userRepository: Repository, private readonly httpService: HttpService, + private readonly environmentService: EnvironmentService, ) {} async challenge(challengeInput: ChallengeInput) { @@ -114,6 +116,12 @@ export class AuthService { ForbiddenException, ); } else { + assert( + !this.environmentService.isSignUpDisabled(), + 'Sign up is disabled', + ForbiddenException, + ); + const workspaceToCreate = this.workspaceRepository.create({ displayName: '', domainName: '', diff --git a/packages/twenty-server/src/core/client-config/client-config.entity.ts b/packages/twenty-server/src/core/client-config/client-config.entity.ts index c4fd3a836..a9c2d150b 100644 --- a/packages/twenty-server/src/core/client-config/client-config.entity.ts +++ b/packages/twenty-server/src/core/client-config/client-config.entity.ts @@ -59,6 +59,9 @@ export class ClientConfig { @Field(() => Boolean) signInPrefilled: boolean; + @Field(() => Boolean) + signUpDisabled: boolean; + @Field(() => Boolean) debugMode: boolean; diff --git a/packages/twenty-server/src/core/client-config/client-config.resolver.ts b/packages/twenty-server/src/core/client-config/client-config.resolver.ts index 120b271a3..c75a8b6ec 100644 --- a/packages/twenty-server/src/core/client-config/client-config.resolver.ts +++ b/packages/twenty-server/src/core/client-config/client-config.resolver.ts @@ -26,6 +26,7 @@ export class ClientConfigResolver { billingUrl: this.environmentService.getBillingUrl(), }, signInPrefilled: this.environmentService.isSignInPrefilled(), + signUpDisabled: this.environmentService.isSignUpDisabled(), debugMode: this.environmentService.isDebugMode(), support: { supportDriver: this.environmentService.getSupportDriver(), diff --git a/packages/twenty-server/src/filters/utils/global-exception-handler.util.ts b/packages/twenty-server/src/filters/utils/global-exception-handler.util.ts index ccab30479..ac5390ee0 100644 --- a/packages/twenty-server/src/filters/utils/global-exception-handler.util.ts +++ b/packages/twenty-server/src/filters/utils/global-exception-handler.util.ts @@ -5,6 +5,7 @@ import { BaseGraphQLError, ForbiddenError, ValidationError, + NotFoundError, } from 'src/filters/utils/graphql-errors.util'; import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service'; @@ -12,6 +13,7 @@ const graphQLPredefinedExceptions = { 400: ValidationError, 401: AuthenticationError, 403: ForbiddenError, + 404: NotFoundError, }; export const handleExceptionAndConvertToGraphQLError = ( diff --git a/packages/twenty-server/src/filters/utils/graphql-errors.util.ts b/packages/twenty-server/src/filters/utils/graphql-errors.util.ts index 8a4122bb1..d9329d435 100644 --- a/packages/twenty-server/src/filters/utils/graphql-errors.util.ts +++ b/packages/twenty-server/src/filters/utils/graphql-errors.util.ts @@ -133,3 +133,11 @@ export class UserInputError extends BaseGraphQLError { Object.defineProperty(this, 'name', { value: 'UserInputError' }); } } + +export class NotFoundError extends BaseGraphQLError { + constructor(message: string, extensions?: Record) { + super(message, 'NOT_FOUND', extensions); + + Object.defineProperty(this, 'name', { value: 'NotFoundError' }); + } +} diff --git a/packages/twenty-server/src/integrations/environment/environment.service.ts b/packages/twenty-server/src/integrations/environment/environment.service.ts index 7a6b1f318..f9cdb7c4e 100644 --- a/packages/twenty-server/src/integrations/environment/environment.service.ts +++ b/packages/twenty-server/src/integrations/environment/environment.service.ts @@ -244,4 +244,8 @@ export class EnvironmentService { getOpenRouterApiKey(): string | undefined { return this.configService.get('OPENROUTER_API_KEY'); } + + isSignUpDisabled(): boolean { + return this.configService.get('IS_SIGN_UP_DISABLED') ?? false; + } } diff --git a/packages/twenty-server/src/integrations/environment/environment.validation.ts b/packages/twenty-server/src/integrations/environment/environment.validation.ts index ddf69c52e..c67f22bba 100644 --- a/packages/twenty-server/src/integrations/environment/environment.validation.ts +++ b/packages/twenty-server/src/integrations/environment/environment.validation.ts @@ -170,6 +170,11 @@ export class EnvironmentVariables { ) @IsString() SENTRY_DSN?: string; + + @CastToBoolean() + @IsOptional() + @IsBoolean() + IS_SIGN_UP_DISABLED?: boolean; } export const validate = (config: Record) => {