diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index d25651a6ca..9b360b5703 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -490,6 +490,10 @@ export class AuthResolver { @AuthUser() currentUser: User, @AuthProvider() authProvider: AuthProviderEnum, ): Promise { + await this.signInUpService.checkWorkspaceCreationIsAllowedOrThrow( + currentUser, + ); + const { user, workspace } = await this.signInUpService.signUpOnNewWorkspace( { type: 'existingUser', existingUser: currentUser }, ); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts index c2b46d9f32..8443501f9e 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts @@ -355,6 +355,36 @@ export class SignInUpService { return { canImpersonate: false, canAccessFullAdminPanel: false }; } + private isWorkspaceCreationLimitedToServerAdmins(): boolean { + return this.twentyConfigService.get( + 'IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS', + ); + } + + private async isFirstWorkspaceForUser(userId: string): Promise { + const count = await this.userWorkspaceService.countUserWorkspaces(userId); + + return count === 0; + } + + async checkWorkspaceCreationIsAllowedOrThrow( + currentUser: User, + ): Promise { + if (!this.isWorkspaceCreationLimitedToServerAdmins()) return; + + if (await this.isFirstWorkspaceForUser(currentUser.id)) return; + + if (!currentUser.canAccessFullAdminPanel) { + throw new AuthException( + 'Workspace creation is restricted to admins', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + { + userFriendlyMessage: t`Workspace creation is restricted to admins`, + }, + ); + } + } + async signUpOnNewWorkspace( userData: ExistingUserOrPartialUserWithPicture['userData'], ) { diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts index b5a2470864..d67240d023 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts @@ -358,6 +358,15 @@ export class ConfigVariables { }) EMAIL_SMTP_PASSWORD: string; + @ConfigVariablesMetadata({ + group: ConfigVariablesGroup.Other, + description: + 'When enabled, only server admins can create new workspaces. Ignored during initial setup when no workspace exists.', + type: ConfigVariableType.BOOLEAN, + }) + @IsOptional() + IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS = false; + @ConfigVariablesMetadata({ group: ConfigVariablesGroup.StorageConfig, description: 'Type of storage to use (local or S3)', diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index 9a47aa11c3..f81c189d44 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -217,6 +217,10 @@ export class UserWorkspaceService extends TypeOrmQueryService { return workspace; } + async countUserWorkspaces(userId: string): Promise { + return await this.userWorkspaceRepository.count({ where: { userId } }); + } + async findAvailableWorkspacesByEmail(email: string) { const user = await this.userRepository.findOne({ where: { diff --git a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts index b325daa703..022e4de899 100644 --- a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts @@ -90,7 +90,7 @@ describe('roles permissions', () => { expect(resp.status).toBe(200); expect(resp.body.errors).toBeUndefined(); - expect(resp.body.data.getRoles).toHaveLength(5); + expect(resp.body.data.getRoles).toHaveLength(7); const roles = resp.body.data.getRoles; const guestRole = roles.find((role: any) => role.label === 'Guest');