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 <charles@twenty.com>
This commit is contained in:
Arthur EICHELBERGER
2024-01-11 11:48:14 +01:00
committed by GitHub
parent 66a054ac21
commit c6ae480856
16 changed files with 60 additions and 1 deletions

View File

@@ -56,6 +56,7 @@ import TabItem from '@theme/TabItem';
['AUTH_GOOGLE_CLIENT_SECRET', '', 'Google client secret'], ['AUTH_GOOGLE_CLIENT_SECRET', '', 'Google client secret'],
['AUTH_GOOGLE_CALLBACK_URL', '', 'Google auth callback'], ['AUTH_GOOGLE_CALLBACK_URL', '', 'Google auth callback'],
['FRONT_AUTH_CALLBACK_URL', 'http://localhost:3001/verify ', 'Callback used for Login page'], ['FRONT_AUTH_CALLBACK_URL', 'http://localhost:3001/verify ', 'Callback used for Login page'],
['IS_SIGN_UP_DISABLED', 'false', 'Disable sign-up'],
]}></OptionTable> ]}></OptionTable>
### Email ### Email

View File

@@ -1,10 +1,12 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { matchPath, useLocation, useNavigate } from 'react-router-dom'; import { matchPath, useLocation, useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { useEventTracker } from '@/analytics/hooks/useEventTracker'; import { useEventTracker } from '@/analytics/hooks/useEventTracker';
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus'; import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus'; import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandType } from '@/command-menu/types/Command'; import { CommandType } from '@/command-menu/types/Command';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
@@ -41,6 +43,8 @@ export const PageChangeEffect = () => {
const openCreateActivity = useOpenCreateActivityDrawer(); const openCreateActivity = useOpenCreateActivityDrawer();
const isSignUpDisabled = useRecoilValue(isSignUpDisabledState);
useEffect(() => { useEffect(() => {
if (!previousLocation || previousLocation !== location.pathname) { if (!previousLocation || previousLocation !== location.pathname) {
setPreviousLocation(location.pathname); setPreviousLocation(location.pathname);
@@ -115,10 +119,13 @@ export const PageChangeEffect = () => {
navigateToSignUp(); navigateToSignUp();
}, },
}); });
} else if (isMatchingLocation(AppPath.SignUp) && isSignUpDisabled) {
navigate(AppPath.SignIn);
} }
}, [ }, [
enqueueSnackBar, enqueueSnackBar,
isMatchingLocation, isMatchingLocation,
isSignUpDisabled,
location.pathname, location.pathname,
navigate, navigate,
onboardingStatus, onboardingStatus,

View File

@@ -72,6 +72,7 @@ export type ClientConfig = {
debugMode: Scalars['Boolean']; debugMode: Scalars['Boolean'];
sentry: Sentry; sentry: Sentry;
signInPrefilled: Scalars['Boolean']; signInPrefilled: Scalars['Boolean'];
signUpDisabled: Scalars['Boolean'];
support: Support; support: Support;
telemetry: Telemetry; telemetry: Telemetry;
}; };
@@ -746,7 +747,7 @@ export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; 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<{ export type UploadFileMutationVariables = Exact<{
file: Scalars['Upload']; file: Scalars['Upload'];
@@ -1281,6 +1282,7 @@ export const GetClientConfigDocument = gql`
billingUrl billingUrl
} }
signInPrefilled signInPrefilled
signUpDisabled
debugMode debugMode
telemetry { telemetry {
enabled enabled

View File

@@ -5,6 +5,7 @@ import { authProvidersState } from '@/client-config/states/authProvidersState';
import { billingState } from '@/client-config/states/billingState'; import { billingState } from '@/client-config/states/billingState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState';
import { sentryConfigState } from '@/client-config/states/sentryConfigState'; import { sentryConfigState } from '@/client-config/states/sentryConfigState';
import { supportChatState } from '@/client-config/states/supportChatState'; import { supportChatState } from '@/client-config/states/supportChatState';
import { telemetryState } from '@/client-config/states/telemetryState'; import { telemetryState } from '@/client-config/states/telemetryState';
@@ -17,6 +18,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
const setIsDebugMode = useSetRecoilState(isDebugModeState); const setIsDebugMode = useSetRecoilState(isDebugModeState);
const setIsSignInPrefilled = useSetRecoilState(isSignInPrefilledState); const setIsSignInPrefilled = useSetRecoilState(isSignInPrefilledState);
const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState);
const setBilling = useSetRecoilState(billingState); const setBilling = useSetRecoilState(billingState);
const setTelemetry = useSetRecoilState(telemetryState); const setTelemetry = useSetRecoilState(telemetryState);
@@ -35,6 +37,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
}); });
setIsDebugMode(data?.clientConfig.debugMode); setIsDebugMode(data?.clientConfig.debugMode);
setIsSignInPrefilled(data?.clientConfig.signInPrefilled); setIsSignInPrefilled(data?.clientConfig.signInPrefilled);
setIsSignUpDisabled(data?.clientConfig.signUpDisabled);
setBilling(data?.clientConfig.billing); setBilling(data?.clientConfig.billing);
setTelemetry(data?.clientConfig.telemetry); setTelemetry(data?.clientConfig.telemetry);
@@ -49,6 +52,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
setAuthProviders, setAuthProviders,
setIsDebugMode, setIsDebugMode,
setIsSignInPrefilled, setIsSignInPrefilled,
setIsSignUpDisabled,
setTelemetry, setTelemetry,
setSupportChat, setSupportChat,
setBilling, setBilling,

View File

@@ -12,6 +12,7 @@ export const GET_CLIENT_CONFIG = gql`
billingUrl billingUrl
} }
signInPrefilled signInPrefilled
signUpDisabled
debugMode debugMode
telemetry { telemetry {
enabled enabled

View File

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

View File

@@ -1,5 +1,6 @@
export const mockedClientConfig = { export const mockedClientConfig = {
signInPrefilled: true, signInPrefilled: true,
signUpDisabled: false,
dataModelSettingsEnabled: true, dataModelSettingsEnabled: true,
developersSettingsEnabled: true, developersSettingsEnabled: true,
debugMode: false, debugMode: false,

View File

@@ -21,6 +21,7 @@ SIGN_IN_PREFILLED=true
# MESSAGING_PROVIDER_GMAIL_ENABLED=false # MESSAGING_PROVIDER_GMAIL_ENABLED=false
# IS_BILLING_ENABLED=false # IS_BILLING_ENABLED=false
# BILLING_PLAN_REQUIRED_LINK=https://twenty.com/stripe-redirection # 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_ID=replace_me_with_google_client_id
# AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret # AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret
# AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect # AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect

View File

@@ -7,6 +7,7 @@ import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspa
import { FileUploadService } from 'src/core/file/services/file-upload.service'; import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { Workspace } from 'src/core/workspace/workspace.entity'; import { Workspace } from 'src/core/workspace/workspace.entity';
import { User } from 'src/core/user/user.entity'; import { User } from 'src/core/user/user.entity';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { TokenService } from './token.service'; import { TokenService } from './token.service';
@@ -46,6 +47,10 @@ describe('AuthService', () => {
provide: getRepositoryToken(User, 'core'), provide: getRepositoryToken(User, 'core'),
useValue: {}, useValue: {},
}, },
{
provide: EnvironmentService,
useValue: {},
},
], ],
}).compile(); }).compile();

View File

@@ -29,6 +29,7 @@ import { UserService } from 'src/core/user/services/user.service';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service'; import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { getImageBufferFromUrl } from 'src/utils/image'; import { getImageBufferFromUrl } from 'src/utils/image';
import { FileUploadService } from 'src/core/file/services/file-upload.service'; import { FileUploadService } from 'src/core/file/services/file-upload.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { TokenService } from './token.service'; import { TokenService } from './token.service';
@@ -50,6 +51,7 @@ export class AuthService {
@InjectRepository(User, 'core') @InjectRepository(User, 'core')
private readonly userRepository: Repository<User>, private readonly userRepository: Repository<User>,
private readonly httpService: HttpService, private readonly httpService: HttpService,
private readonly environmentService: EnvironmentService,
) {} ) {}
async challenge(challengeInput: ChallengeInput) { async challenge(challengeInput: ChallengeInput) {
@@ -114,6 +116,12 @@ export class AuthService {
ForbiddenException, ForbiddenException,
); );
} else { } else {
assert(
!this.environmentService.isSignUpDisabled(),
'Sign up is disabled',
ForbiddenException,
);
const workspaceToCreate = this.workspaceRepository.create({ const workspaceToCreate = this.workspaceRepository.create({
displayName: '', displayName: '',
domainName: '', domainName: '',

View File

@@ -59,6 +59,9 @@ export class ClientConfig {
@Field(() => Boolean) @Field(() => Boolean)
signInPrefilled: boolean; signInPrefilled: boolean;
@Field(() => Boolean)
signUpDisabled: boolean;
@Field(() => Boolean) @Field(() => Boolean)
debugMode: boolean; debugMode: boolean;

View File

@@ -26,6 +26,7 @@ export class ClientConfigResolver {
billingUrl: this.environmentService.getBillingUrl(), billingUrl: this.environmentService.getBillingUrl(),
}, },
signInPrefilled: this.environmentService.isSignInPrefilled(), signInPrefilled: this.environmentService.isSignInPrefilled(),
signUpDisabled: this.environmentService.isSignUpDisabled(),
debugMode: this.environmentService.isDebugMode(), debugMode: this.environmentService.isDebugMode(),
support: { support: {
supportDriver: this.environmentService.getSupportDriver(), supportDriver: this.environmentService.getSupportDriver(),

View File

@@ -5,6 +5,7 @@ import {
BaseGraphQLError, BaseGraphQLError,
ForbiddenError, ForbiddenError,
ValidationError, ValidationError,
NotFoundError,
} from 'src/filters/utils/graphql-errors.util'; } from 'src/filters/utils/graphql-errors.util';
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service'; import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
@@ -12,6 +13,7 @@ const graphQLPredefinedExceptions = {
400: ValidationError, 400: ValidationError,
401: AuthenticationError, 401: AuthenticationError,
403: ForbiddenError, 403: ForbiddenError,
404: NotFoundError,
}; };
export const handleExceptionAndConvertToGraphQLError = ( export const handleExceptionAndConvertToGraphQLError = (

View File

@@ -133,3 +133,11 @@ export class UserInputError extends BaseGraphQLError {
Object.defineProperty(this, 'name', { value: 'UserInputError' }); Object.defineProperty(this, 'name', { value: 'UserInputError' });
} }
} }
export class NotFoundError extends BaseGraphQLError {
constructor(message: string, extensions?: Record<string, any>) {
super(message, 'NOT_FOUND', extensions);
Object.defineProperty(this, 'name', { value: 'NotFoundError' });
}
}

View File

@@ -244,4 +244,8 @@ export class EnvironmentService {
getOpenRouterApiKey(): string | undefined { getOpenRouterApiKey(): string | undefined {
return this.configService.get<string | undefined>('OPENROUTER_API_KEY'); return this.configService.get<string | undefined>('OPENROUTER_API_KEY');
} }
isSignUpDisabled(): boolean {
return this.configService.get<boolean>('IS_SIGN_UP_DISABLED') ?? false;
}
} }

View File

@@ -170,6 +170,11 @@ export class EnvironmentVariables {
) )
@IsString() @IsString()
SENTRY_DSN?: string; SENTRY_DSN?: string;
@CastToBoolean()
@IsOptional()
@IsBoolean()
IS_SIGN_UP_DISABLED?: boolean;
} }
export const validate = (config: Record<string, unknown>) => { export const validate = (config: Record<string, unknown>) => {