mirror of
https://github.com/twentyhq/twenty.git
synced 2026-05-19 05:53:23 -04:00
Enforce workspace count limit for multi-workspace setups (#19036)
## Summary - Limits workspace creation to 5 workspaces per server when no valid enterprise key is configured - Enterprise key validity is checked first (synchronous, in-memory cached) to avoid unnecessary DB queries on enterprise deployments - Adds 4 unit tests covering: limit enforcement, enterprise key bypass, below-limit creation, and performance (no enterprise check when workspace count is zero) ## Test plan - [x] All 11 unit tests pass (8 existing + 3 new behavioral + 1 performance assertion) - [ ] Manual: verify workspace creation blocked at limit=5 without enterprise key - [ ] Manual: verify workspace creation allowed beyond limit with valid enterprise key - [ ] Manual: verify first workspace (bootstrap) creation is unaffected Made with [Cursor](https://cursor.com)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export const MAX_WORKSPACES_WITHOUT_ENTERPRISE_KEY = 5;
|
||||
@@ -97,6 +97,9 @@ const createSignInUpServiceForTests = () => {
|
||||
{
|
||||
uploadWorkspaceLogoFromUrl: jest.fn(),
|
||||
} as any,
|
||||
{
|
||||
isValid: jest.fn().mockReturnValue(false),
|
||||
} as any,
|
||||
{
|
||||
createQueryRunner: jest.fn(() => queryRunnerMock),
|
||||
} as any,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Repository, type DataSource, type QueryRunner } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
|
||||
import { MAX_WORKSPACES_WITHOUT_ENTERPRISE_KEY } from 'src/engine/core-modules/auth/constants/max-workspaces-without-enterprise-key.constants';
|
||||
import { type AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import {
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
type SignInUpNewUserPayload,
|
||||
} from 'src/engine/core-modules/auth/types/signInUp.type';
|
||||
import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service';
|
||||
import { EnterprisePlanService } from 'src/engine/core-modules/enterprise/services/enterprise-plan.service';
|
||||
import { FileCorePictureService } from 'src/engine/core-modules/file/file-core-picture/services/file-core-picture.service';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
@@ -65,6 +67,7 @@ export class SignInUpService {
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly fileCorePictureService: FileCorePictureService,
|
||||
private readonly enterprisePlanService: EnterprisePlanService,
|
||||
@InjectDataSource()
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
@@ -424,6 +427,8 @@ export class SignInUpService {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.assertWorkspaceCountWithinLimit(workspaceCount);
|
||||
|
||||
if (
|
||||
!this.twentyConfigService.get(
|
||||
'IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS',
|
||||
@@ -449,6 +454,26 @@ export class SignInUpService {
|
||||
);
|
||||
}
|
||||
|
||||
private async assertWorkspaceCountWithinLimit(
|
||||
workspaceCount: number,
|
||||
): Promise<void> {
|
||||
if (this.enterprisePlanService.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (workspaceCount < MAX_WORKSPACES_WITHOUT_ENTERPRISE_KEY) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new AuthException(
|
||||
`Cannot create more than ${MAX_WORKSPACES_WITHOUT_ENTERPRISE_KEY} workspaces without a valid enterprise key`,
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
{
|
||||
userFriendlyMessage: msg`Workspace limit reached. A valid enterprise key is required to create more workspaces.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async signUpOnNewWorkspace(
|
||||
userData: ExistingUserOrPartialUserWithPicture['userData'],
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user