mirror of
https://github.com/lingble/twenty.git
synced 2025-11-28 03:43:35 +00:00
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:
committed by
GitHub
parent
66a054ac21
commit
c6ae480856
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const GET_CLIENT_CONFIG = gql`
|
|||||||
billingUrl
|
billingUrl
|
||||||
}
|
}
|
||||||
signInPrefilled
|
signInPrefilled
|
||||||
|
signUpDisabled
|
||||||
debugMode
|
debugMode
|
||||||
telemetry {
|
telemetry {
|
||||||
enabled
|
enabled
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const isSignUpDisabledState = atom<boolean>({
|
||||||
|
key: 'isSignUpDisabledState',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user