fix: Add reserved subdomains constant and update validation on generateSubdomain (#15217)

## Description

Fixes #15160 

- Moved the reserved subdomains to a separate shared constant file:
`packages/twenty-server/src/engine/core-modules/workspace/constants/reserved-subdomains.constant.ts`
- Updated the validation while generating subdomain to check if the
extracted subdomain (from email or display name) is reserved
- When a reserved subdomain is detected, the server will automatically
fall back to a random subdomain

---------

Co-authored-by: Naineel Soyantar <naineelsoyantar@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Naineel Soyantar
2025-10-21 20:23:48 +05:30
committed by GitHub
parent 139b6dd29b
commit 26e432d8e2
97 changed files with 1369 additions and 754 deletions
@@ -13,8 +13,9 @@ import { parseCoreBatchPath } from 'src/engine/api/rest/core/query-builder/utils
import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils';
import { type Query } from 'src/engine/api/rest/core/types/query.type';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { getObjectMetadataMapItemByNamePlural } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util';
@@ -23,7 +24,6 @@ import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/works
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
@Injectable()
export class CoreQueryBuilderFactory {
@@ -33,7 +33,7 @@ export class CoreQueryBuilderFactory {
private readonly findDuplicatesQueryFactory: FindDuplicatesQueryFactory,
private readonly findDuplicatesVariablesFactory: FindDuplicatesVariablesFactory,
private readonly accessTokenService: AccessTokenService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly featureFlagService: FeatureFlagService,
@@ -69,7 +69,7 @@ export class CoreQueryBuilderFactory {
if (!objectMetadataMaps) {
throw new BadRequestException(
`No object was found for the workspace associated with this API key. You may generate a new one here ${this.domainManagerService
`No object was found for the workspace associated with this API key. You may generate a new one here ${this.workspaceDomainsService
.buildWorkspaceURL({
workspace,
pathname: getSettingsPath(SettingsPath.ApiWebhooks),
@@ -4,7 +4,7 @@ import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/
import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories';
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
@@ -14,7 +14,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
imports: [
AuthModule,
ApiKeyModule,
DomainManagerModule,
WorkspaceDomainsModule,
FeatureFlagModule,
WorkspaceCacheStorageModule,
WorkspaceMetadataCacheModule,
@@ -19,7 +19,6 @@ import { RestApiService } from 'src/engine/api/rest/rest-api.service';
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
@@ -54,7 +53,6 @@ const restApiCoreResolvers = [
ActorModule,
FeatureFlagModule,
CoreCommonApiModule,
DomainManagerModule,
],
controllers: [RestApiCoreController],
providers: [
@@ -6,7 +6,7 @@ import axios from 'axios';
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { User } from 'src/engine/core-modules/user/user.entity';
@@ -59,7 +59,7 @@ describe('AdminPanelService', () => {
},
},
{
provide: DomainManagerService,
provide: WorkspaceDomainsService,
useValue: {
getWorkspaceUrls: jest.fn().mockReturnValue({
customUrl: undefined,
@@ -8,7 +8,7 @@ import { AdminPanelResolver } from 'src/engine/core-modules/admin-panel/admin-pa
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { HealthModule } from 'src/engine/core-modules/health/health.module';
@@ -22,8 +22,8 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
imports: [
TypeOrmModule.forFeature([User]),
AuthModule,
DomainManagerModule,
FileModule,
WorkspaceDomainsModule,
HealthModule,
RedisClientModule,
TerminusModule,
@@ -12,13 +12,11 @@ import { type ConfigVariablesGroupDataDTO } from 'src/engine/core-modules/admin-
import { type ConfigVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/config-variables.output';
import { type UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity';
import { type VersionInfoDTO } from 'src/engine/core-modules/admin-panel/dtos/version-info.dto';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { type FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
@@ -32,10 +30,8 @@ import { userValidator } from 'src/engine/core-modules/user/user.validate';
@Injectable()
export class AdminPanelService {
constructor(
private readonly loginTokenService: LoginTokenService,
private readonly twentyConfigService: TwentyConfigService,
private readonly domainManagerService: DomainManagerService,
private readonly auditService: AuditService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly fileService: FileService,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@@ -90,7 +86,7 @@ export class AdminPanelService {
})
: userWorkspace.workspace.logo,
allowImpersonation: userWorkspace.workspace.allowImpersonation,
workspaceUrls: this.domainManagerService.getWorkspaceUrls({
workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls({
subdomain: userWorkspace.workspace.subdomain,
customDomain: userWorkspace.workspace.customDomain,
isCustomDomainEnabled: userWorkspace.workspace.isCustomDomainEnabled,
@@ -5,12 +5,12 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { ApprovedAccessDomain } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
import { ApprovedAccessDomainResolver } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.resolver';
import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
@Module({
imports: [
DomainManagerModule,
WorkspaceDomainsModule,
FileModule,
NestjsQueryTypeOrmModule.forFeature([ApprovedAccessDomain]),
],
@@ -16,7 +16,7 @@ import {
ApprovedAccessDomainExceptionCode,
} from 'src/engine/core-modules/approved-access-domain/approved-access-domain.exception';
import { approvedAccessDomainValidator } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.validate';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -31,8 +31,8 @@ export class ApprovedAccessDomainService {
private readonly approvedAccessDomainRepository: Repository<ApprovedAccessDomainEntity>,
private readonly emailService: EmailService,
private readonly twentyConfigService: TwentyConfigService,
private readonly domainManagerService: DomainManagerService,
private readonly fileService: FileService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
) {}
async sendApprovedAccessDomainValidationEmail(
@@ -61,7 +61,7 @@ export class ApprovedAccessDomainService {
);
}
const link = this.domainManagerService.buildWorkspaceURL({
const link = this.workspaceDomainsService.buildWorkspaceURL({
workspace,
pathname: getSettingsPath(SettingsPath.Domains),
searchParams: {
@@ -10,7 +10,7 @@ import {
ApprovedAccessDomainException,
ApprovedAccessDomainExceptionCode,
} from 'src/engine/core-modules/approved-access-domain/approved-access-domain.exception';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -35,7 +35,7 @@ describe('ApprovedAccessDomainService', () => {
let approvedAccessDomainRepository: Repository<ApprovedAccessDomain>;
let emailService: EmailService;
let twentyConfigService: TwentyConfigService;
let domainManagerService: DomainManagerService;
let workspaceDomainsService: WorkspaceDomainsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -63,7 +63,7 @@ describe('ApprovedAccessDomainService', () => {
},
},
{
provide: DomainManagerService,
provide: WorkspaceDomainsService,
useValue: {
buildWorkspaceURL: jest.fn(),
},
@@ -87,8 +87,9 @@ describe('ApprovedAccessDomainService', () => {
);
emailService = module.get<EmailService>(EmailService);
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
domainManagerService =
module.get<DomainManagerService>(DomainManagerService);
workspaceDomainsService = module.get<WorkspaceDomainsService>(
WorkspaceDomainsService,
);
});
describe('createApprovedAccessDomain', () => {
@@ -286,7 +287,7 @@ describe('ApprovedAccessDomainService', () => {
.mockResolvedValue(approvedAccessDomain);
jest
.spyOn(domainManagerService, 'buildWorkspaceURL')
.spyOn(workspaceDomainsService, 'buildWorkspaceURL')
.mockReturnValue(new URL('https://sub.twenty.com'));
jest
@@ -303,7 +304,7 @@ describe('ApprovedAccessDomainService', () => {
approvedAccessDomain,
);
expect(domainManagerService.buildWorkspaceURL).toHaveBeenCalledWith({
expect(workspaceDomainsService.buildWorkspaceURL).toHaveBeenCalledWith({
workspace: workspace,
pathname: getSettingsPath(SettingsPath.Domains),
searchParams: { validationToken: expect.any(String) },
@@ -33,7 +33,9 @@ import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/l
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module';
import { SubdomainManagerModule } from 'src/engine/core-modules/domain/subdomain-manager/subdomain-manager.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { EmailVerificationModule } from 'src/engine/core-modules/email-verification/email-verification.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
@@ -74,7 +76,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
JwtModule,
FileUploadModule,
DataSourceModule,
DomainManagerModule,
WorkspaceDomainsModule,
TokenModule,
UserModule,
WorkspaceManagerModule,
@@ -109,6 +111,8 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
TwoFactorAuthenticationModule,
ApiKeyModule,
AuditModule,
SubdomainManagerModule,
DomainServerConfigModule,
],
controllers: [
GoogleAuthController,
@@ -9,7 +9,7 @@ import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-u
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service';
import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
@@ -64,7 +64,7 @@ describe('AuthResolver', () => {
useValue: {},
},
{
provide: DomainManagerService,
provide: WorkspaceDomainsService,
useValue: {
buildWorkspaceURL: jest
.fn()
@@ -49,7 +49,7 @@ import {
} from 'src/engine/core-modules/auth/types/auth-context.type';
import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard';
import { CaptchaGraphqlApiExceptionFilter } from 'src/engine/core-modules/captcha/filters/captcha-graphql-api-exception.filter';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { EmailVerificationExceptionFilter } from 'src/engine/core-modules/email-verification/email-verification-exception-filter.util';
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
@@ -61,6 +61,7 @@ import { TwoFactorAuthenticationExceptionFilter } from 'src/engine/core-modules/
import { TwoFactorAuthenticationService } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -74,7 +75,6 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
import { LoginToken } from './dto/login-token.entity';
@@ -115,8 +115,7 @@ export class AuthResolver {
private signInUpService: SignInUpService,
private transientTokenService: TransientTokenService,
private emailVerificationService: EmailVerificationService,
// private oauthService: OAuthService,
private domainManagerService: DomainManagerService,
private workspaceDomainsService: WorkspaceDomainsService,
private userWorkspaceService: UserWorkspaceService,
private emailVerificationTokenService: EmailVerificationTokenService,
private sSOService: SSOService,
@@ -173,7 +172,7 @@ export class AuthResolver {
@Args('origin') origin: string,
): Promise<LoginToken> {
const workspace =
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
origin,
);
@@ -252,7 +251,7 @@ export class AuthResolver {
);
const workspace =
(await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
(await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
origin,
)) ??
(await this.userWorkspaceService.findFirstWorkspaceByUserId(
@@ -268,7 +267,8 @@ export class AuthResolver {
authProvider,
);
const workspaceUrls = this.domainManagerService.getWorkspaceUrls(workspace);
const workspaceUrls =
this.workspaceDomainsService.getWorkspaceUrls(workspace);
return { loginToken, workspaceUrls };
}
@@ -330,7 +330,7 @@ export class AuthResolver {
);
const workspace =
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
origin,
);
@@ -475,7 +475,7 @@ export class AuthResolver {
loginToken,
workspace: {
id: workspace.id,
workspaceUrls: this.domainManagerService.getWorkspaceUrls(workspace),
workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls(workspace),
},
};
}
@@ -504,20 +504,11 @@ export class AuthResolver {
loginToken,
workspace: {
id: workspace.id,
workspaceUrls: this.domainManagerService.getWorkspaceUrls(workspace),
workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls(workspace),
},
};
}
// @Mutation(() => ExchangeAuthCode)
// async exchangeAuthorizationCode(
// @Args() exchangeAuthCodeInput: ExchangeAuthCodeInput,
// ) {
// return await this.oauthService.verifyAuthorizationCode(
// exchangeAuthCodeInput,
// );
// }
@Mutation(() => TransientToken)
@UseGuards(UserAuthGuard)
async generateTransientToken(
@@ -606,7 +597,7 @@ export class AuthResolver {
tokenWorkspaceId: string,
): Promise<Workspace> {
const workspace =
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
origin,
);
@@ -23,12 +23,12 @@ import { GoogleAPIsOauthRequestCodeGuard } from 'src/engine/core-modules/auth/gu
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
@Controller('auth/google-apis')
@UseFilters(AuthRestApiExceptionFilter)
@@ -38,7 +38,7 @@ export class GoogleAPIsAuthController {
private readonly transientTokenService: TransientTokenService,
private readonly twentyConfigService: TwentyConfigService,
private readonly onboardingService: OnboardingService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly guardRedirectService: GuardRedirectService,
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
@@ -120,7 +120,7 @@ export class GoogleAPIsAuthController {
connectedAccountId,
});
const url = this.domainManagerService.buildWorkspaceURL({
const url = this.workspaceDomainsService.buildWorkspaceURL({
workspace,
pathname,
});
@@ -23,7 +23,7 @@ import { MicrosoftAPIsOauthRequestCodeGuard } from 'src/engine/core-modules/auth
import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { MicrosoftAPIsRequest } from 'src/engine/core-modules/auth/types/microsoft-api-request.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -37,7 +37,7 @@ export class MicrosoftAPIsAuthController {
private readonly microsoftAPIsService: MicrosoftAPIsService,
private readonly transientTokenService: TransientTokenService,
private readonly twentyConfigService: TwentyConfigService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly onboardingService: OnboardingService,
private readonly guardRedirectService: GuardRedirectService,
@InjectRepository(Workspace)
@@ -127,7 +127,7 @@ export class MicrosoftAPIsAuthController {
connectedAccountId,
});
const url = this.domainManagerService.buildWorkspaceURL({
const url = this.workspaceDomainsService.buildWorkspaceURL({
workspace,
pathname,
});
@@ -14,8 +14,8 @@ import { InjectRepository } from '@nestjs/typeorm';
import { generateServiceProviderMetadata } from '@node-saml/node-saml';
import { Response } from 'express';
import { AppPath } from 'twenty-shared/types';
import { Repository } from 'typeorm';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import {
AuthException,
@@ -29,17 +29,17 @@ import { AuthService } from 'src/engine/core-modules/auth/services/auth.service'
import { OIDCRequest } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy';
import { SAMLRequest } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import {
IdentityProviderType,
WorkspaceSSOIdentityProvider,
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
@Controller('auth')
@UseFilters(AuthRestApiExceptionFilter)
@@ -48,7 +48,7 @@ export class SSOAuthController {
private readonly loginTokenService: LoginTokenService,
private readonly authService: AuthService,
private readonly guardRedirectService: GuardRedirectService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly userService: UserService,
private readonly sSOService: SSOService,
@InjectRepository(WorkspaceSSOIdentityProvider)
@@ -158,7 +158,7 @@ export class SSOAuthController {
this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
error,
workspace:
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
workspaceIdentityProvider?.workspace,
),
pathname: AppPath.Verify,
@@ -10,13 +10,13 @@ import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
@Catch(AuthException)
export class AuthOAuthExceptionFilter implements ExceptionFilter {
constructor(
private readonly domainManagerService: DomainManagerService,
private readonly domainServerConfigService: DomainServerConfigService,
private readonly httpExceptionHandlerService: HttpExceptionHandlerService,
) {}
@@ -28,7 +28,7 @@ export class AuthOAuthExceptionFilter implements ExceptionFilter {
case AuthExceptionCode.OAUTH_ACCESS_DENIED:
response
.status(403)
.redirect(this.domainManagerService.getBaseUrl().toString());
.redirect(this.domainServerConfigService.getBaseUrl().toString());
break;
default:
return this.httpExceptionHandlerService.handleError(
@@ -11,7 +11,7 @@ import {
import { GoogleAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -24,9 +24,9 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
private readonly guardRedirectService: GuardRedirectService,
private readonly twentyConfigService: TwentyConfigService,
private readonly transientTokenService: TransientTokenService,
private readonly domainManagerService: DomainManagerService,
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
private readonly workspaceDomainsService: WorkspaceDomainsService,
) {
super();
}
@@ -66,7 +66,7 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
);
const redirectErrorUrl =
this.domainManagerService.computeRedirectErrorUrl(
this.workspaceDomainsService.computeWorkspaceRedirectErrorUrl(
'We cannot connect to your Google account, please try again with more permissions, or a valid account',
{
subdomain: workspace.subdomain,
@@ -11,7 +11,7 @@ import {
import { GoogleAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -24,7 +24,7 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') {
private readonly guardRedirectService: GuardRedirectService,
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
) {
super({
prompt: 'select_account',
@@ -73,7 +73,7 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') {
this.guardRedirectService.dispatchErrorFromGuard(
context,
err,
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
workspace,
),
);
@@ -2,16 +2,16 @@ import { type ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { type Request } from 'express';
import { Repository } from 'typeorm';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
@Injectable()
export class GoogleOauthGuard extends AuthGuard('google') {
@@ -19,7 +19,7 @@ export class GoogleOauthGuard extends AuthGuard('google') {
private readonly guardRedirectService: GuardRedirectService,
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
) {
super({
prompt: 'select_account',
@@ -53,7 +53,7 @@ export class GoogleOauthGuard extends AuthGuard('google') {
this.guardRedirectService.dispatchErrorFromGuard(
context,
err,
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
workspace,
),
);
@@ -11,10 +11,10 @@ import {
import { MicrosoftAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth/strategies/microsoft-apis-oauth-request-code.auth.strategy';
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
@Injectable()
export class MicrosoftAPIsOauthRequestCodeGuard extends AuthGuard(
@@ -26,7 +26,7 @@ export class MicrosoftAPIsOauthRequestCodeGuard extends AuthGuard(
private readonly guardRedirectService: GuardRedirectService,
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
) {
super({
prompt: 'select_account',
@@ -72,7 +72,7 @@ export class MicrosoftAPIsOauthRequestCodeGuard extends AuthGuard(
this.guardRedirectService.dispatchErrorFromGuard(
context,
err,
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
workspace,
),
);
@@ -4,9 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
@Injectable()
export class MicrosoftOAuthGuard extends AuthGuard('microsoft') {
@@ -14,7 +14,7 @@ export class MicrosoftOAuthGuard extends AuthGuard('microsoft') {
private readonly guardRedirectService: GuardRedirectService,
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
) {
super({
prompt: 'select_account',
@@ -41,7 +41,7 @@ export class MicrosoftOAuthGuard extends AuthGuard('microsoft') {
this.guardRedirectService.dispatchErrorFromGuard(
context,
err,
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
workspace,
),
);
@@ -10,18 +10,18 @@ import {
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { OIDCAuthStrategy } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { type SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
import { type WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
@Injectable()
export class OIDCAuthGuard extends AuthGuard('openidconnect') {
constructor(
private readonly sSOService: SSOService,
private readonly guardRedirectService: GuardRedirectService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
) {
super();
}
@@ -91,7 +91,7 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') {
this.guardRedirectService.dispatchErrorFromGuard(
context,
err,
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
identityProvider?.workspace,
),
);
@@ -1,15 +1,15 @@
import { type ExecutionContext } from '@nestjs/common';
import { Test, type TestingModule } from '@nestjs/testing';
import { AuthGuard } from '@nestjs/passport';
import { Test, type TestingModule } from '@nestjs/testing';
import { type Issuer } from 'openid-client';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { OIDCAuthGuard } from 'src/engine/core-modules/auth/guards/oidc-auth.guard';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { type SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
import { type WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
const createMockExecutionContext = (mockedRequest: any): ExecutionContext => {
return {
@@ -60,7 +60,7 @@ describe('OIDCAuthGuard', () => {
},
},
{
provide: DomainManagerService,
provide: WorkspaceDomainsService,
useValue: {
getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain:
jest.fn(),
@@ -10,18 +10,18 @@ import {
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { type SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
import { type WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
@Injectable()
export class SAMLAuthGuard extends AuthGuard('saml') {
constructor(
private readonly sSOService: SSOService,
private readonly guardRedirectService: GuardRedirectService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
) {
super();
}
@@ -51,7 +51,7 @@ export class SAMLAuthGuard extends AuthGuard('saml') {
this.guardRedirectService.dispatchErrorFromGuard(
context,
err,
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
identityProvider?.workspace,
),
);
@@ -17,7 +17,8 @@ import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/l
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service';
import { type ExistingUserOrNewUser } from 'src/engine/core-modules/auth/types/signInUp.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
@@ -76,7 +77,11 @@ describe('AuthService', () => {
useValue: {},
},
{
provide: DomainManagerService,
provide: WorkspaceDomainsService,
useValue: {},
},
{
provide: DomainServerConfigService,
useValue: {},
},
{
@@ -50,19 +50,20 @@ import {
type SignInUpBaseParams,
type SignInUpNewUserPayload,
} from 'src/engine/core-modules/auth/types/signInUp.type';
import { type WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { WorkspaceDomainConfig } from 'src/engine/core-modules/domain/workspace-domains/types/workspace-domain-config.type';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
@Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
@@ -70,7 +71,8 @@ export class AuthService {
constructor(
private readonly accessTokenService: AccessTokenService,
private readonly workspaceAgnosticTokenService: WorkspaceAgnosticTokenService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly domainServerConfigService: DomainServerConfigService,
private readonly refreshTokenService: RefreshTokenService,
private readonly loginTokenService: LoginTokenService,
private readonly guardRedirectService: GuardRedirectService,
@@ -535,7 +537,7 @@ export class AuthService {
const emailTemplate = PasswordUpdateNotifyEmail({
userName: `${user.firstName} ${user.lastName}`,
email: user.email,
link: this.domainManagerService.getBaseUrl().toString(),
link: this.domainServerConfigService.getBaseUrl().toString(),
locale: firstUserWorkspace.locale,
});
@@ -582,10 +584,10 @@ export class AuthService {
billingCheckoutSessionState,
}: {
loginToken: string;
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType;
workspace: WorkspaceDomainConfig;
billingCheckoutSessionState?: string;
}) {
const url = this.domainManagerService.buildWorkspaceURL({
const url = this.workspaceDomainsService.buildWorkspaceURL({
workspace,
pathname: AppPath.Verify,
searchParams: {
@@ -796,7 +798,7 @@ export class AuthService {
},
));
const url = this.domainManagerService.buildBaseUrl({
const url = this.domainServerConfigService.buildBaseUrl({
pathname: AppPath.SignInUp,
searchParams: {
tokenPair: JSON.stringify({
@@ -882,7 +884,7 @@ export class AuthService {
return this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({
error,
workspace:
this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
currentWorkspace,
),
pathname: AppPath.Verify,
@@ -12,13 +12,14 @@ import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { type User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { ResetPasswordService } from './reset-password.service';
@@ -40,7 +41,7 @@ describe('ResetPasswordService', () => {
let appTokenRepository: Repository<AppToken>;
let emailService: EmailService;
let twentyConfigService: TwentyConfigService;
let domainManagerService: DomainManagerService;
let workspaceDomainsService: WorkspaceDomainsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -72,11 +73,16 @@ describe('ResetPasswordService', () => {
},
},
{
provide: DomainManagerService,
provide: DomainServerConfigService,
useValue: {
getBaseUrl: jest
.fn()
.mockResolvedValue(new URL('http://localhost:3001')),
},
},
{
provide: WorkspaceDomainsService,
useValue: {
buildWorkspaceURL: jest.fn(),
},
},
@@ -107,8 +113,10 @@ describe('ResetPasswordService', () => {
);
emailService = module.get<EmailService>(EmailService);
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
domainManagerService =
module.get<DomainManagerService>(DomainManagerService);
workspaceDomainsService = module.get<WorkspaceDomainsService>(
WorkspaceDomainsService,
);
});
it('should be defined', () => {
@@ -197,7 +205,7 @@ describe('ResetPasswordService', () => {
.spyOn(twentyConfigService, 'get')
.mockReturnValue('http://localhost:3000');
jest
.spyOn(domainManagerService, 'buildWorkspaceURL')
.spyOn(workspaceDomainsService, 'buildWorkspaceURL')
.mockReturnValue(
new URL(
'https://subdomain.localhost.com:3000/reset-password/passwordResetToken',
@@ -25,19 +25,19 @@ import { type EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/em
import { type InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity';
import { type PasswordResetToken } from 'src/engine/core-modules/auth/dto/token.entity';
import { type ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
@Injectable()
export class ResetPasswordService {
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(AppToken)
@@ -127,7 +127,7 @@ export class ResetPasswordService {
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
const link = this.domainManagerService.buildWorkspaceURL({
const link = this.workspaceDomainsService.buildWorkspaceURL({
workspace,
pathname: getAppPath(AppPath.ResetPassword, {
passwordResetToken: resetToken.passwordResetToken,
@@ -17,7 +17,7 @@ import {
type ExistingUserOrPartialUserWithPicture,
type SignInUpBaseParams,
} from 'src/engine/core-modules/auth/types/signInUp.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
@@ -45,7 +45,7 @@ describe('SignInUpService', () => {
let workspaceInvitationService: WorkspaceInvitationService;
let userWorkspaceService: UserWorkspaceService;
let twentyConfigService: TwentyConfigService;
let domainManagerService: DomainManagerService;
let subdomainManagerService: SubdomainManagerService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -120,7 +120,7 @@ describe('SignInUpService', () => {
},
},
{
provide: DomainManagerService,
provide: SubdomainManagerService,
useValue: {
generateSubdomain: jest.fn(),
},
@@ -155,8 +155,9 @@ describe('SignInUpService', () => {
userWorkspaceService =
module.get<UserWorkspaceService>(UserWorkspaceService);
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
domainManagerService =
module.get<DomainManagerService>(DomainManagerService);
subdomainManagerService = module.get<SubdomainManagerService>(
SubdomainManagerService,
);
});
it('should handle signInUp with valid personal invitation', async () => {
@@ -271,7 +272,7 @@ describe('SignInUpService', () => {
} as Workspace);
jest.spyOn(UserRepository, 'create').mockReturnValue({} as User);
jest
.spyOn(domainManagerService, 'generateSubdomain')
.spyOn(subdomainManagerService, 'generateSubdomain')
.mockResolvedValue('a-subdomain');
jest
.spyOn(UserRepository, 'save')
@@ -26,7 +26,7 @@ import {
type SignInUpBaseParams,
type SignInUpNewUserPayload,
} from 'src/engine/core-modules/auth/types/signInUp.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
@@ -53,7 +53,7 @@ export class SignInUpService {
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
private readonly httpService: HttpService,
private readonly twentyConfigService: TwentyConfigService,
private readonly domainManagerService: DomainManagerService,
private readonly subdomainManagerService: SubdomainManagerService,
private readonly userService: UserService,
) {}
@@ -423,8 +423,8 @@ export class SignInUpService {
isWorkEmailFound && (await isLogoUrlValid()) ? logoUrl : undefined;
const workspaceToCreate = this.workspaceRepository.create({
subdomain: await this.domainManagerService.generateSubdomain(
isWorkEmailFound ? { email } : {},
subdomain: await this.subdomainManagerService.generateSubdomain(
isWorkEmailFound ? { userEmail: email } : {},
),
displayName: '',
inviteHash: v4(),
@@ -8,8 +8,8 @@ import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billin
import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-invoice.service';
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-price.service';
import { BillingWebhookProductService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-product.service';
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service';
import { BillingWebhookSubscriptionScheduleService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription-schedule.service';
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
@@ -19,7 +19,6 @@ import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module';
@@ -32,7 +31,6 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
imports: [
FeatureFlagModule,
StripeModule,
DomainManagerModule,
MessageQueueModule,
PermissionsModule,
WorkspaceModule,
@@ -20,30 +20,30 @@ import { BillingFeatureUsedListener } from 'src/engine/core-modules/billing/list
import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener';
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
import { BillingPriceService } from 'src/engine/core-modules/billing/services/billing-price.service';
import { BillingProductService } from 'src/engine/core-modules/billing/services/billing-product.service';
import { BillingSubscriptionItemService } from 'src/engine/core-modules/billing/services/billing-subscription-item.service';
import { BillingSubscriptionPhaseService } from 'src/engine/core-modules/billing/services/billing-subscription-phase.service';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { BillingSubscriptionPhaseService } from 'src/engine/core-modules/billing/services/billing-subscription-phase.service';
import { BillingPriceService } from 'src/engine/core-modules/billing/services/billing-price.service';
@Module({
imports: [
FeatureFlagModule,
StripeModule,
DomainManagerModule,
MessageQueueModule,
PermissionsModule,
AiModule,
WorkspaceDomainsModule,
TypeOrmModule.forFeature([
BillingSubscription,
BillingSubscriptionItem,
@@ -12,21 +12,21 @@ import {
BillingException,
BillingExceptionCode,
} from 'src/engine/core-modules/billing/billing.exception';
import { billingValidator } from 'src/engine/core-modules/billing/billing.validate';
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { StripeBillingPortalService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service';
import { StripeCheckoutService } from 'src/engine/core-modules/billing/stripe/services/stripe-checkout.service';
import { type BillingGetPricesPerPlanResult } from 'src/engine/core-modules/billing/types/billing-get-prices-per-plan-result.type';
import { BillingMeterPrice } from 'src/engine/core-modules/billing/types/billing-meter-price.type';
import { type BillingPortalCheckoutSessionParameters } from 'src/engine/core-modules/billing/types/billing-portal-checkout-session-parameters.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { assert } from 'src/utils/assert';
import { billingValidator } from 'src/engine/core-modules/billing/billing.validate';
import { BillingMeterPrice } from 'src/engine/core-modules/billing/types/billing-meter-price.type';
import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
@Injectable()
export class BillingPortalWorkspaceService {
@@ -34,7 +34,7 @@ export class BillingPortalWorkspaceService {
constructor(
private readonly stripeCheckoutService: StripeCheckoutService,
private readonly stripeBillingPortalService: StripeBillingPortalService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly billingSubscriptionService: BillingSubscriptionService,
@InjectRepository(BillingSubscription)
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
@@ -127,7 +127,7 @@ export class BillingPortalWorkspaceService {
billingPricesPerPlan: BillingGetPricesPerPlanResult;
successUrlPath?: string;
}) {
const frontBaseUrl = this.domainManagerService.buildWorkspaceURL({
const frontBaseUrl = this.workspaceDomainsService.buildWorkspaceURL({
workspace,
});
const cancelUrl = frontBaseUrl.toString();
@@ -182,7 +182,7 @@ export class BillingPortalWorkspaceService {
throw new Error('Error: missing stripeCustomerId');
}
const frontBaseUrl = this.domainManagerService.buildWorkspaceURL({
const frontBaseUrl = this.workspaceDomainsService.buildWorkspaceURL({
workspace,
});
@@ -5,7 +5,7 @@ import { Injectable, Logger } from '@nestjs/common';
import type Stripe from 'stripe';
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
@@ -15,7 +15,7 @@ export class StripeBillingPortalService {
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly domainManagerService: DomainManagerService,
private readonly domainServerConfigService: DomainServerConfigService,
private readonly stripeSDKService: StripeSDKService,
) {
if (!this.twentyConfigService.get('IS_BILLING_ENABLED')) {
@@ -33,7 +33,7 @@ export class StripeBillingPortalService {
return await this.stripe.billingPortal.sessions.create({
customer: stripeCustomerId,
return_url:
returnUrl ?? this.domainManagerService.getBaseUrl().toString(),
returnUrl ?? this.domainServerConfigService.getBaseUrl().toString(),
});
}
}
@@ -4,6 +4,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
import { StripeBillingAlertService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-alert.service';
import { StripeBillingMeterEventService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter-event.service';
import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service';
import { StripeBillingPortalService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service';
@@ -12,16 +13,15 @@ import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/se
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
import { StripeProductService } from 'src/engine/core-modules/billing/stripe/services/stripe-product.service';
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service';
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
import { StripeSubscriptionScheduleService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-schedule.service';
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
import { StripeWebhookService } from 'src/engine/core-modules/billing/stripe/services/stripe-webhook.service';
import { StripeSDKModule } from 'src/engine/core-modules/billing/stripe/stripe-sdk/stripe-sdk.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { StripeBillingAlertService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-alert.service';
import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module';
@Module({
imports: [
DomainManagerModule,
DomainServerConfigModule,
StripeSDKModule,
TypeOrmModule.forFeature([BillingCustomer]),
],
@@ -1,13 +1,13 @@
import { Module } from '@nestjs/common';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module';
import { ClientConfigController } from './client-config.controller';
import { ClientConfigService } from './services/client-config.service';
@Module({
imports: [DomainManagerModule],
imports: [DomainServerConfigModule],
controllers: [ClientConfigController],
providers: [ClientConfigService],
})
@@ -6,14 +6,14 @@ import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
describe('ClientConfigService', () => {
let service: ClientConfigService;
let twentyConfigService: TwentyConfigService;
let domainManagerService: DomainManagerService;
let domainServerConfigService: DomainServerConfigService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -26,7 +26,7 @@ describe('ClientConfigService', () => {
},
},
{
provide: DomainManagerService,
provide: DomainServerConfigService,
useValue: {
getFrontUrl: jest.fn(),
},
@@ -42,8 +42,9 @@ describe('ClientConfigService', () => {
service = module.get<ClientConfigService>(ClientConfigService);
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
domainManagerService =
module.get<DomainManagerService>(DomainManagerService);
domainServerConfigService = module.get<DomainServerConfigService>(
DomainServerConfigService,
);
});
it('should be defined', () => {
@@ -91,7 +92,7 @@ describe('ClientConfigService', () => {
return mockValues[key];
});
jest.spyOn(domainManagerService, 'getFrontUrl').mockReturnValue({
jest.spyOn(domainServerConfigService, 'getFrontUrl').mockReturnValue({
hostname: 'app.twenty.com',
} as URL);
});
@@ -15,7 +15,7 @@ import {
type ClientAIModelConfig,
type ClientConfig,
} from 'src/engine/core-modules/client-config/client-config.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -23,7 +23,7 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent
export class ClientConfigService {
constructor(
private twentyConfigService: TwentyConfigService,
private domainManagerService: DomainManagerService,
private domainServerConfigService: DomainServerConfigService,
private aiModelRegistryService: AiModelRegistryService,
) {}
@@ -107,7 +107,7 @@ export class ClientConfigService {
'IS_EMAIL_VERIFICATION_REQUIRED',
),
defaultSubdomain: this.twentyConfigService.get('DEFAULT_SUBDOMAIN'),
frontDomain: this.domainManagerService.getFrontUrl().hostname,
frontDomain: this.domainServerConfigService.getFrontUrl().hostname,
debugMode:
this.twentyConfigService.get('NODE_ENV') ===
NodeEnvironment.DEVELOPMENT,
@@ -2,17 +2,19 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { PublicDomainModule } from 'src/engine/core-modules/public-domain/public-domain.module';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { DnsCloudflareController } from 'src/engine/core-modules/cloudflare/controllers/dns-cloudflare.controller';
import { DnsCloudflareService } from 'src/engine/core-modules/cloudflare/services/dns-cloudflare.service';
import { CustomDomainManagerModule } from 'src/engine/core-modules/domain/custom-domain-manager/custom-domain-manager.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { PublicDomainModule } from 'src/engine/core-modules/public-domain/public-domain.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Module({
imports: [
NestjsQueryTypeOrmModule.forFeature([PublicDomain, Workspace]),
WorkspaceModule,
WorkspaceDomainsModule,
CustomDomainManagerModule,
PublicDomainModule,
],
providers: [DnsCloudflareService],
@@ -2,22 +2,26 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
import { CustomDomainManagerService } from 'src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { PublicDomainService } from 'src/engine/core-modules/public-domain/public-domain.service';
@Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
export class DnsCloudflareService {
constructor(
private readonly workspaceService: WorkspaceService,
private readonly publicDomainService: PublicDomainService,
private readonly customDomainManagerService: CustomDomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
) {}
async checkHostname(hostname: string) {
const workspace = await this.workspaceService.findByCustomDomain(hostname);
const workspace =
await this.workspaceDomainsService.findByCustomDomain(hostname);
if (isDefined(workspace)) {
await this.workspaceService.checkCustomDomainValidRecords(workspace);
await this.customDomainManagerService.checkCustomDomainValidRecords(
workspace,
);
}
const publicDomain = await this.publicDomainService.findByDomain(hostname);
@@ -1,10 +1,9 @@
import { Module } from '@nestjs/common';
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module';
@Module({
imports: [DomainManagerModule],
imports: [DomainServerConfigModule],
providers: [DnsManagerService],
exports: [DnsManagerService],
})
@@ -6,18 +6,18 @@ import { type CustomHostnameCreateResponse } from 'cloudflare/resources/custom-h
import { AuditContextMock } from 'test/utils/audit-context.mock';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { DnsManagerException } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager.exception';
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
import { DnsManagerException } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager.exception';
jest.mock('cloudflare');
describe('DnsManagerService', () => {
let dnsManagerService: DnsManagerService;
let twentyConfigService: TwentyConfigService;
let domainManagerService: DomainManagerService;
let domainServerConfigService: DomainServerConfigService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -36,7 +36,7 @@ describe('DnsManagerService', () => {
},
},
{
provide: DomainManagerService,
provide: DomainServerConfigService,
useValue: {
getBaseUrl: jest.fn(),
getPublicDomainUrl: jest.fn(),
@@ -53,8 +53,9 @@ describe('DnsManagerService', () => {
dnsManagerService = module.get<DnsManagerService>(DnsManagerService);
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
domainManagerService =
module.get<DomainManagerService>(DomainManagerService);
domainServerConfigService = module.get<DomainServerConfigService>(
DomainServerConfigService,
);
(dnsManagerService as any).cloudflareClient = {
customHostnames: {
@@ -165,7 +166,7 @@ describe('DnsManagerService', () => {
jest.spyOn(twentyConfigService, 'get').mockReturnValue('test-zone-id');
jest
.spyOn(domainManagerService, 'getBaseUrl')
.spyOn(domainServerConfigService, 'getBaseUrl')
.mockReturnValue(new URL('https://front.domain'));
(dnsManagerService as any).cloudflareClient = cloudflareMock;
@@ -204,7 +205,7 @@ describe('DnsManagerService', () => {
jest.spyOn(twentyConfigService, 'get').mockReturnValue('test-zone-id');
jest
.spyOn(domainManagerService, 'getBaseUrl')
.spyOn(domainServerConfigService, 'getBaseUrl')
.mockReturnValue(new URL('https://front.domain'));
(dnsManagerService as any).cloudflareClient = cloudflareMock;
@@ -245,11 +246,11 @@ describe('DnsManagerService', () => {
jest.spyOn(twentyConfigService, 'get').mockReturnValue('test-zone-id');
jest
.spyOn(domainManagerService, 'getBaseUrl')
.spyOn(domainServerConfigService, 'getBaseUrl')
.mockReturnValue(new URL('https://front.domain'));
jest
.spyOn(domainManagerService, 'getPublicDomainUrl')
.spyOn(domainServerConfigService, 'getPublicDomainUrl')
.mockReturnValue(new URL('https://front.public-domain'));
(dnsManagerService as any).cloudflareClient = cloudflareMock;
@@ -15,7 +15,7 @@ import {
DnsManagerExceptionCode,
} from 'src/engine/core-modules/dns-manager/exceptions/dns-manager.exception';
import { dnsManagerValidator } from 'src/engine/core-modules/dns-manager/validator/dns-manager.validate';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
type DnsManagerOptions = {
@@ -28,7 +28,7 @@ export class DnsManagerService {
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly domainManagerService: DomainManagerService,
private readonly domainServerConfigService: DomainServerConfigService,
) {
if (this.twentyConfigService.get('CLOUDFLARE_API_KEY')) {
this.cloudflareClient = new Cloudflare({
@@ -61,7 +61,7 @@ export class DnsManagerService {
): Promise<DomainValidRecords | undefined> {
if (
options?.isPublicDomain &&
!isDefined(this.domainManagerService.getPublicDomainUrl().hostname)
!isDefined(this.domainServerConfigService.getPublicDomainUrl().hostname)
) {
throw new DnsManagerException(
'Missing public domain URL',
@@ -95,8 +95,8 @@ export class DnsManagerService {
status: statuses.redirection,
key: hostname,
value: options?.isPublicDomain
? this.domainManagerService.getPublicDomainUrl().hostname
: this.domainManagerService.getBaseUrl().hostname,
? this.domainServerConfigService.getPublicDomainUrl().hostname
: this.domainServerConfigService.getBaseUrl().hostname,
},
{
validationType: 'ssl' as const,
@@ -1,14 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
@Module({
imports: [TypeOrmModule.forFeature([Workspace, PublicDomain]), AuditModule],
providers: [DomainManagerService],
exports: [DomainManagerService],
})
export class DomainManagerModule {}
@@ -0,0 +1,21 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { DnsManagerModule } from 'src/engine/core-modules/dns-manager/dns-manager.module';
import { CustomDomainManagerService } from 'src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service';
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace, PublicDomain]),
DnsManagerModule,
BillingModule,
AuditModule,
],
providers: [CustomDomainManagerService],
exports: [CustomDomainManagerService],
})
export class CustomDomainManagerModule {}
@@ -0,0 +1,127 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { msg } from '@lingui/core/macro';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated';
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records';
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import {
WorkspaceException,
WorkspaceExceptionCode,
} from 'src/engine/core-modules/workspace/workspace.exception';
@Injectable()
export class CustomDomainManagerService {
constructor(
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(PublicDomain)
private readonly publicDomainRepository: Repository<PublicDomain>,
private readonly billingService: BillingService,
private readonly dnsManagerService: DnsManagerService,
private readonly auditService: AuditService,
) {}
async isCustomDomainEnabled(workspaceId: string) {
const isCustomDomainBillingEnabled =
await this.billingService.hasEntitlement(
workspaceId,
BillingEntitlementKey.CUSTOM_DOMAIN,
);
if (!isCustomDomainBillingEnabled) {
throw new WorkspaceException(
`No entitlement found for this workspace`,
WorkspaceExceptionCode.WORKSPACE_CUSTOM_DOMAIN_DISABLED,
);
}
}
async setCustomDomain(workspace: Workspace, customDomain: string) {
await this.isCustomDomainEnabled(workspace.id);
const existingWorkspace = await this.workspaceRepository.findOne({
where: { customDomain },
});
if (existingWorkspace && existingWorkspace.id !== workspace.id) {
throw new WorkspaceException(
'Domain already taken',
WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN,
);
}
if (
await this.publicDomainRepository.findOneBy({
domain: customDomain,
})
) {
throw new WorkspaceException(
'Domain is already registered as public domain',
WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN,
{
userFriendlyMessage: msg`Domain is already registered as public domain`,
},
);
}
if (!isDefined(customDomain) || workspace.customDomain === customDomain) {
return;
}
if (isDefined(workspace.customDomain)) {
await this.dnsManagerService.updateHostname(
workspace.customDomain,
customDomain,
);
} else {
await this.dnsManagerService.registerHostname(customDomain);
}
}
async checkCustomDomainValidRecords(
workspace: Workspace,
domainValidRecord?: DomainValidRecords,
) {
assertIsDefinedOrThrow(workspace.customDomain);
const customDomainWithRecords =
domainValidRecord ??
(await this.dnsManagerService.getHostnameWithRecords(
workspace.customDomain,
));
assertIsDefinedOrThrow(customDomainWithRecords);
const isCustomDomainWorking =
await this.dnsManagerService.isHostnameWorking(workspace.customDomain);
if (workspace.isCustomDomainEnabled !== isCustomDomainWorking) {
workspace.isCustomDomainEnabled = isCustomDomainWorking;
await this.workspaceRepository.save(workspace);
const analytics = this.auditService.createContext({
workspaceId: workspace.id,
});
analytics.insertWorkspaceEvent(
workspace.isCustomDomainEnabled
? CUSTOM_DOMAIN_ACTIVATED_EVENT
: CUSTOM_DOMAIN_DEACTIVATED_EVENT,
{},
);
}
return customDomainWithRecords;
}
}
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
@Module({
imports: [],
providers: [DomainServerConfigService],
exports: [DomainServerConfigService],
})
export class DomainServerConfigModule {}
@@ -0,0 +1,82 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
describe('SubdomainManagerService', () => {
let domainServerConfigService: DomainServerConfigService;
let twentyConfigService: TwentyConfigService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DomainServerConfigService,
{
provide: getRepositoryToken(Workspace),
useValue: {
find: jest.fn(),
findOne: jest.fn(),
},
},
{
provide: getRepositoryToken(PublicDomain),
useValue: {
findOne: jest.fn(),
},
},
{
provide: TwentyConfigService,
useValue: {
get: jest.fn(),
},
},
],
}).compile();
domainServerConfigService = module.get<DomainServerConfigService>(
DomainServerConfigService,
);
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
});
describe('buildBaseUrl', () => {
it('should build the base URL from environment variables', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
const env = {
FRONTEND_URL: 'https://example.com',
};
// @ts-expect-error legacy noImplicitAny
return env[key];
});
const result = domainServerConfigService.getBaseUrl();
expect(result.toString()).toBe('https://example.com/');
});
it('should append default subdomain if multiworkspace is enabled', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
const env = {
FRONTEND_URL: 'https://example.com',
IS_MULTIWORKSPACE_ENABLED: true,
DEFAULT_SUBDOMAIN: 'test',
};
// @ts-expect-error legacy noImplicitAny
return env[key];
});
const result = domainServerConfigService.getBaseUrl();
expect(result.toString()).toBe('https://test.example.com/');
});
});
});
@@ -0,0 +1,69 @@
import { Injectable } from '@nestjs/common';
import { buildUrlWithPathnameAndSearchParams } from 'src/engine/core-modules/domain/domain-server-config/utils/build-url-with-pathname-and-search-params.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@Injectable()
export class DomainServerConfigService {
constructor(private readonly twentyConfigService: TwentyConfigService) {}
getFrontUrl() {
return new URL(
this.twentyConfigService.get('FRONTEND_URL') ??
this.twentyConfigService.get('SERVER_URL'),
);
}
getBaseUrl(): URL {
const baseUrl = this.getFrontUrl();
if (
this.twentyConfigService.get('IS_MULTIWORKSPACE_ENABLED') &&
this.twentyConfigService.get('DEFAULT_SUBDOMAIN')
) {
baseUrl.hostname = `${this.twentyConfigService.get('DEFAULT_SUBDOMAIN')}.${baseUrl.hostname}`;
}
return baseUrl;
}
getPublicDomainUrl(): URL {
return new URL(this.twentyConfigService.get('PUBLIC_DOMAIN_URL'));
}
buildBaseUrl({
pathname,
searchParams,
}: {
pathname?: string;
searchParams?: Record<string, string | number>;
}) {
return buildUrlWithPathnameAndSearchParams({
baseUrl: this.getBaseUrl(),
pathname,
searchParams,
});
}
getSubdomainAndDomainFromUrl = (url: string) => {
const { hostname: originHostname } = new URL(url);
const frontDomain = this.getFrontUrl().hostname;
const isFrontdomain = originHostname.endsWith(`.${frontDomain}`);
const subdomain = originHostname.replace(`.${frontDomain}`, '');
return {
subdomain:
isFrontdomain && !this.isDefaultSubdomain(subdomain)
? subdomain
: undefined,
domain: isFrontdomain ? null : originHostname,
};
};
isDefaultSubdomain(subdomain: string) {
return subdomain === this.twentyConfigService.get('DEFAULT_SUBDOMAIN');
}
}
@@ -0,0 +1,8 @@
export const appendSearchParamsToUrl = (
url: URL,
searchParams: Record<string, string | number | boolean>,
) => {
Object.entries(searchParams).forEach(([key, value]) => {
url.searchParams.set(key, value.toString());
});
};
@@ -0,0 +1,25 @@
import { appendSearchParamsToUrl } from 'src/engine/core-modules/domain/domain-server-config/utils/append-search-params-to-url.util';
type BuildUrlWithPathnameAndSearchParamsProps = {
baseUrl: URL;
pathname?: string;
searchParams?: Record<string, string | number | boolean>;
};
export const buildUrlWithPathnameAndSearchParams = ({
baseUrl,
pathname,
searchParams,
}: BuildUrlWithPathnameAndSearchParamsProps) => {
const url = baseUrl;
if (pathname) {
url.pathname = pathname;
}
if (searchParams) {
appendSearchParamsToUrl(url, searchParams);
}
return url;
};
@@ -0,0 +1,85 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { generateRandomSubdomain } from 'src/engine/core-modules/domain/subdomain-manager/utils/generate-random-subdomain.util';
import { getSubdomainFromEmail } from 'src/engine/core-modules/domain/subdomain-manager/utils/get-subdomain-from-email.util';
import { getSubdomainNameFromDisplayName } from 'src/engine/core-modules/domain/subdomain-manager/utils/get-subdomain-name-from-display-name.util';
import { isSubdomainValid } from 'src/engine/core-modules/domain/subdomain-manager/utils/is-subdomain-valid.util';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import {
WorkspaceException,
WorkspaceExceptionCode,
} from 'src/engine/core-modules/workspace/workspace.exception';
@Injectable()
export class SubdomainManagerService {
constructor(
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
private readonly twentyConfigService: TwentyConfigService,
) {}
async generateSubdomain({
userEmail,
workspaceDisplayName,
}: {
userEmail?: string;
workspaceDisplayName?: string;
}) {
const subdomainFromUserEmail = getSubdomainFromEmail(userEmail);
const subdomainFromWorkspaceDisplayName =
getSubdomainNameFromDisplayName(workspaceDisplayName);
const extractedSubdomain =
subdomainFromUserEmail || subdomainFromWorkspaceDisplayName;
const isExtractedSubdomainValid = isDefined(extractedSubdomain)
? isSubdomainValid(extractedSubdomain)
: false;
const subdomain = isExtractedSubdomainValid
? extractedSubdomain
: generateRandomSubdomain();
const existingWorkspaceCount = await this.workspaceRepository.countBy({
subdomain,
});
return `${subdomain}${existingWorkspaceCount > 0 ? `-${Math.random().toString(36).substring(2, 10)}` : ''}`;
}
async isSubdomainAvailable(subdomain: string) {
const existingWorkspace = await this.workspaceRepository.findOne({
where: { subdomain: subdomain },
});
return !existingWorkspace;
}
async validateSubdomainOrThrow(subdomain: string) {
const isValid = isSubdomainValid(subdomain);
if (!isValid) {
throw new WorkspaceException(
'Subdomain is not valid',
WorkspaceExceptionCode.SUBDOMAIN_NOT_VALID,
);
}
const isAvailable = await this.isSubdomainAvailable(subdomain);
if (
!isAvailable ||
this.twentyConfigService.get('DEFAULT_SUBDOMAIN') === subdomain
) {
throw new WorkspaceException(
'Subdomain already taken',
WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN,
);
}
}
}
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Module({
imports: [TypeOrmModule.forFeature([Workspace])],
providers: [SubdomainManagerService],
exports: [SubdomainManagerService],
})
export class SubdomainManagerModule {}
@@ -1,4 +1,4 @@
import { generateRandomSubdomain } from 'src/engine/core-modules/domain-manager/utils/generate-random-subdomain';
import { generateRandomSubdomain } from 'src/engine/core-modules/domain/subdomain-manager/utils/generate-random-subdomain.util';
describe('generateRandomSubdomain', () => {
it('should return a string in the format "prefix-color-suffix"', () => {
@@ -1,4 +1,4 @@
import { getSubdomainFromEmail } from 'src/engine/core-modules/domain-manager/utils/get-subdomain-from-email';
import { getSubdomainFromEmail } from 'src/engine/core-modules/domain/subdomain-manager/utils/get-subdomain-from-email.util';
describe('getSubdomainFromEmail', () => {
beforeEach(() => {
@@ -1,4 +1,4 @@
import { getSubdomainNameFromDisplayName } from 'src/engine/core-modules/domain-manager/utils/get-subdomain-name-from-display-name';
import { getSubdomainNameFromDisplayName } from 'src/engine/core-modules/domain/subdomain-manager/utils/get-subdomain-name-from-display-name.util';
describe('getSubdomainNameFromDisplayName', () => {
it('should return a hyphen-separated, lowercase subdomain name without spaces for a valid display name', () => {
@@ -0,0 +1,384 @@
import { isSubdomainValid } from 'src/engine/core-modules/domain/subdomain-manager/utils/is-subdomain-valid.util';
describe('isSubdomainValid', () => {
describe('valid subdomains', () => {
it('should accept valid alphanumeric subdomains', () => {
expect(isSubdomainValid('abc')).toBe(true);
expect(isSubdomainValid('test123')).toBe(true);
expect(isSubdomainValid('company1')).toBe(true);
expect(isSubdomainValid('workspace2024')).toBe(true);
});
it('should accept subdomains with hyphens in the middle', () => {
expect(isSubdomainValid('my-company')).toBe(true);
expect(isSubdomainValid('test-workspace')).toBe(true);
expect(isSubdomainValid('multi-word-subdomain')).toBe(true);
expect(isSubdomainValid('a-b-c-d-e')).toBe(true);
});
it('should accept subdomains with mixed alphanumeric and hyphens', () => {
expect(isSubdomainValid('test-123')).toBe(true);
expect(isSubdomainValid('company-2024')).toBe(true);
expect(isSubdomainValid('workspace-v2')).toBe(true);
expect(isSubdomainValid('my-app-123')).toBe(true);
});
it('should accept minimum length subdomains (3 characters)', () => {
expect(isSubdomainValid('abc')).toBe(true);
expect(isSubdomainValid('a1b')).toBe(true);
expect(isSubdomainValid('x-y')).toBe(true);
});
it('should accept maximum length subdomains (30 characters)', () => {
const maxLengthSubdomain = 'a'.repeat(28) + 'bc'; // 30 characters total
expect(isSubdomainValid(maxLengthSubdomain)).toBe(true);
const maxLengthWithHyphens = 'a' + '-'.repeat(28) + 'b'; // 30 characters with hyphens
expect(isSubdomainValid(maxLengthWithHyphens)).toBe(true);
});
it('should accept subdomains starting and ending with alphanumeric characters', () => {
expect(isSubdomainValid('a-b')).toBe(true);
expect(isSubdomainValid('1-test-2')).toBe(true);
expect(isSubdomainValid('start-middle-end')).toBe(true);
});
});
describe('invalid subdomain patterns', () => {
it('should reject empty strings', () => {
expect(isSubdomainValid('')).toBe(false);
});
it('should reject subdomains that are too short (less than 3 characters)', () => {
expect(isSubdomainValid('a')).toBe(false);
expect(isSubdomainValid('ab')).toBe(false);
expect(isSubdomainValid('1')).toBe(false);
expect(isSubdomainValid('12')).toBe(false);
});
it('should reject subdomains that are too long (more than 30 characters)', () => {
const tooLongSubdomain = 'a'.repeat(31);
expect(isSubdomainValid(tooLongSubdomain)).toBe(false);
const wayTooLongSubdomain = 'a'.repeat(50);
expect(isSubdomainValid(wayTooLongSubdomain)).toBe(false);
});
it('should reject subdomains starting with hyphens', () => {
expect(isSubdomainValid('-test')).toBe(false);
expect(isSubdomainValid('-abc')).toBe(false);
expect(isSubdomainValid('-my-company')).toBe(false);
});
it('should reject subdomains ending with hyphens', () => {
expect(isSubdomainValid('test-')).toBe(false);
expect(isSubdomainValid('abc-')).toBe(false);
expect(isSubdomainValid('my-company-')).toBe(false);
});
it('should reject subdomains with uppercase letters', () => {
expect(isSubdomainValid('Test')).toBe(false);
expect(isSubdomainValid('MyCompany')).toBe(false);
expect(isSubdomainValid('WORKSPACE')).toBe(false);
expect(isSubdomainValid('test-Company')).toBe(false);
});
it('should reject subdomains with special characters', () => {
expect(isSubdomainValid('test@company')).toBe(false);
expect(isSubdomainValid('my_workspace')).toBe(false);
expect(isSubdomainValid('test.company')).toBe(false);
expect(isSubdomainValid('workspace#1')).toBe(false);
expect(isSubdomainValid('test$company')).toBe(false);
expect(isSubdomainValid('my%workspace')).toBe(false);
expect(isSubdomainValid('test&company')).toBe(false);
expect(isSubdomainValid('workspace*1')).toBe(false);
expect(isSubdomainValid('test+company')).toBe(false);
expect(isSubdomainValid('my=workspace')).toBe(false);
});
it('should reject subdomains with spaces', () => {
expect(isSubdomainValid('test company')).toBe(false);
expect(isSubdomainValid('my workspace')).toBe(false);
expect(isSubdomainValid(' test')).toBe(false);
expect(isSubdomainValid('test ')).toBe(false);
expect(isSubdomainValid(' ')).toBe(false);
});
it('should reject subdomains starting with "api-"', () => {
expect(isSubdomainValid('api-test')).toBe(false);
expect(isSubdomainValid('api-company')).toBe(false);
expect(isSubdomainValid('api-workspace')).toBe(false);
expect(isSubdomainValid('api-123')).toBe(false);
});
it('should reject subdomains with only hyphens', () => {
expect(isSubdomainValid('---')).toBe(false);
expect(isSubdomainValid('----')).toBe(false);
});
it('should reject subdomains with numbers only at boundaries but invalid patterns', () => {
expect(isSubdomainValid('1-')).toBe(false);
expect(isSubdomainValid('-1')).toBe(false);
});
});
describe('reserved subdomains', () => {
it('should reject common reserved subdomains', () => {
expect(isSubdomainValid('api')).toBe(false);
expect(isSubdomainValid('www')).toBe(false);
expect(isSubdomainValid('admin')).toBe(false);
expect(isSubdomainValid('dashboard')).toBe(false);
expect(isSubdomainValid('billing')).toBe(false);
expect(isSubdomainValid('support')).toBe(false);
});
it('should reject technical reserved subdomains', () => {
expect(isSubdomainValid('db')).toBe(false);
expect(isSubdomainValid('cdn')).toBe(false);
expect(isSubdomainValid('storage')).toBe(false);
expect(isSubdomainValid('files')).toBe(false);
expect(isSubdomainValid('media')).toBe(false);
expect(isSubdomainValid('assets')).toBe(false);
});
it('should reject authentication related reserved subdomains', () => {
expect(isSubdomainValid('auth')).toBe(false);
expect(isSubdomainValid('login')).toBe(false);
expect(isSubdomainValid('signin')).toBe(false);
expect(isSubdomainValid('signup')).toBe(false);
expect(isSubdomainValid('register')).toBe(false);
});
it('should reject business related reserved subdomains', () => {
expect(isSubdomainValid('about')).toBe(false);
expect(isSubdomainValid('contact')).toBe(false);
expect(isSubdomainValid('careers')).toBe(false);
expect(isSubdomainValid('jobs')).toBe(false);
expect(isSubdomainValid('blog')).toBe(false);
expect(isSubdomainValid('news')).toBe(false);
});
it('should reject country code reserved subdomains', () => {
expect(isSubdomainValid('us')).toBe(false);
expect(isSubdomainValid('uk')).toBe(false);
expect(isSubdomainValid('ca')).toBe(false);
expect(isSubdomainValid('au')).toBe(false);
expect(isSubdomainValid('de')).toBe(false);
expect(isSubdomainValid('fr')).toBe(false);
expect(isSubdomainValid('it')).toBe(false);
expect(isSubdomainValid('es')).toBe(false);
});
it('should reject geographic reserved subdomains', () => {
expect(isSubdomainValid('europe')).toBe(false);
expect(isSubdomainValid('asia')).toBe(false);
expect(isSubdomainValid('africa')).toBe(false);
expect(isSubdomainValid('america')).toBe(false);
expect(isSubdomainValid('oceania')).toBe(false);
expect(isSubdomainValid('paris')).toBe(false);
expect(isSubdomainValid('london')).toBe(false);
expect(isSubdomainValid('new-york')).toBe(false);
});
it('should reject environment related reserved subdomains', () => {
expect(isSubdomainValid('dev')).toBe(false);
expect(isSubdomainValid('test')).toBe(false);
expect(isSubdomainValid('testing')).toBe(false);
expect(isSubdomainValid('staging')).toBe(false);
expect(isSubdomainValid('production')).toBe(false);
});
it('should reject reserved subdomains case-insensitively', () => {
expect(isSubdomainValid('API')).toBe(false);
expect(isSubdomainValid('Api')).toBe(false);
expect(isSubdomainValid('WWW')).toBe(false);
expect(isSubdomainValid('Www')).toBe(false);
expect(isSubdomainValid('ADMIN')).toBe(false);
expect(isSubdomainValid('Admin')).toBe(false);
expect(isSubdomainValid('TEST')).toBe(false);
expect(isSubdomainValid('Test')).toBe(false);
});
it('should reject all reserved subdomains from the constant', () => {
const reservedSubdomains = [
'trust',
'demo',
'api',
't',
'companies',
'telemetry',
'logs',
'metrics',
'next',
'main',
'admin',
'dashboard',
'dash',
'billing',
'db',
'favicon',
'www',
'mail',
'docs',
'dev',
'app',
'staging',
'production',
'developer',
'files',
'cdn',
'storage',
'about',
'help',
'support',
'contact',
'privacy',
'terms',
'careers',
'jobs',
'blog',
'news',
'events',
'community',
'forum',
'chat',
'test',
'testing',
'feedback',
'config',
'settings',
'media',
'image',
'audio',
'video',
'images',
'partners',
'partnership',
'partnerships',
'assets',
'login',
'signin',
'signup',
'legal',
'shop',
'merch',
'store',
'auth',
'register',
'payment',
];
reservedSubdomains.forEach((subdomain) => {
expect(isSubdomainValid(subdomain)).toBe(false);
});
});
});
describe('edge cases', () => {
it('should handle whitespace-only strings', () => {
expect(isSubdomainValid(' ')).toBe(false);
expect(isSubdomainValid('\t')).toBe(false);
expect(isSubdomainValid('\n')).toBe(false);
expect(isSubdomainValid('\r')).toBe(false);
});
it('should handle strings with leading/trailing whitespace', () => {
expect(isSubdomainValid(' test')).toBe(false);
expect(isSubdomainValid('test ')).toBe(false);
expect(isSubdomainValid(' test ')).toBe(false);
expect(isSubdomainValid('\ttest\t')).toBe(false);
});
it('should handle boundary length cases precisely', () => {
// Exactly 3 characters (minimum valid)
expect(isSubdomainValid('abc')).toBe(true);
// Exactly 30 characters (maximum valid)
const exactly30Chars = 'a'.repeat(28) + 'bc';
expect(exactly30Chars.length).toBe(30);
expect(isSubdomainValid(exactly30Chars)).toBe(true);
// Exactly 31 characters (first invalid length)
const exactly31Chars = 'a'.repeat(29) + 'bc';
expect(exactly31Chars.length).toBe(31);
expect(isSubdomainValid(exactly31Chars)).toBe(false);
});
it('should validate that reserved subdomains check is case insensitive', () => {
// Test mixed case variations of reserved subdomains
expect(isSubdomainValid('Trust')).toBe(false);
expect(isSubdomainValid('TRUST')).toBe(false);
expect(isSubdomainValid('tRuSt')).toBe(false);
expect(isSubdomainValid('Demo')).toBe(false);
expect(isSubdomainValid('DEMO')).toBe(false);
expect(isSubdomainValid('dEmO')).toBe(false);
});
it('should accept valid subdomains that are similar to reserved ones but not exact matches', () => {
// 'testing' is reserved, but 'testing123' is not
expect(isSubdomainValid('testing123')).toBe(true);
// 'api' is reserved, but 'myapi' is not
expect(isSubdomainValid('myapi')).toBe(true);
// 'admin' is reserved, but 'adminpanel' is not
expect(isSubdomainValid('adminpanel')).toBe(true);
// 'test' is reserved, but 'testapp' is not
expect(isSubdomainValid('testapp')).toBe(true);
});
it('should handle Unicode characters', () => {
expect(isSubdomainValid('tëst')).toBe(false);
expect(isSubdomainValid('tést')).toBe(false);
expect(isSubdomainValid('tèst')).toBe(false);
expect(isSubdomainValid('café')).toBe(false);
expect(isSubdomainValid('naïve')).toBe(false);
});
it('should handle numeric strings', () => {
expect(isSubdomainValid('123')).toBe(true);
expect(isSubdomainValid('456789')).toBe(true);
expect(isSubdomainValid('1-2-3')).toBe(true);
});
});
describe('pattern validation specifics', () => {
it('should enforce the exact regex pattern requirements', () => {
// Test that the pattern requires alphanumeric start and end
expect(isSubdomainValid('a-b')).toBe(true);
expect(isSubdomainValid('1-2')).toBe(true);
expect(isSubdomainValid('test-123')).toBe(true);
// Test that it rejects patterns not matching the regex
expect(isSubdomainValid('-ab')).toBe(false);
expect(isSubdomainValid('ab-')).toBe(false);
});
it('should reject api- prefix specifically', () => {
expect(isSubdomainValid('api-anything')).toBe(false);
expect(isSubdomainValid('api-test')).toBe(false);
expect(isSubdomainValid('api-123')).toBe(false);
// But allow 'api' in other positions
expect(isSubdomainValid('myapi')).toBe(true);
});
it('should validate length constraints from regex', () => {
// The regex pattern is: /^(?!api-).*^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/
// This means: start char + 1-28 middle chars + end char = 3-30 total chars
// 3 chars: start + 1 middle + end
expect(isSubdomainValid('abc')).toBe(true);
// 30 chars: start + 28 middle + end
const thirtyChars = 'a' + 'b'.repeat(28) + 'c';
expect(thirtyChars.length).toBe(30);
expect(isSubdomainValid(thirtyChars)).toBe(true);
});
});
});
@@ -8,5 +8,5 @@ export const getSubdomainFromEmail = (email?: string) => {
const domain = getDomainNameByEmail(email);
return domain.split('.')[0];
return domain.split('.')[0].toLowerCase();
};
@@ -0,0 +1,9 @@
import { RESERVED_SUBDOMAINS } from 'src/engine/core-modules/workspace/constants/reserved-subdomains.constant';
import { VALID_SUBDOMAIN_PATTERN } from 'src/engine/core-modules/workspace/constants/valid-subdomain-pattern.constant';
export const isSubdomainValid = (subdomain: string) => {
return (
VALID_SUBDOMAIN_PATTERN.test(subdomain) &&
!RESERVED_SUBDOMAINS.includes(subdomain.toLowerCase())
);
};
@@ -3,64 +3,14 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { type Repository } from 'typeorm';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { DomainManagerService } from './domain-manager.service';
describe('DomainManagerService', () => {
describe('getWorkspaceUrls', () => {
it('should return a URL containing the correct customDomain if customDomain is provided', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
const env = {
FRONTEND_URL: 'https://example.com',
};
// @ts-expect-error legacy noImplicitAny
return env[key];
});
const result = domainManagerService.getWorkspaceUrls({
subdomain: 'subdomain',
customDomain: 'custom-host.com',
isCustomDomainEnabled: true,
});
expect(result).toEqual({
customUrl: 'https://custom-host.com/',
subdomainUrl: 'https://example.com/',
});
});
it('should return a URL containing the correct subdomain if customDomain is not provided but subdomain is', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
const env = {
FRONTEND_URL: 'https://example.com',
IS_MULTIWORKSPACE_ENABLED: true,
};
// @ts-expect-error legacy noImplicitAny
return env[key];
});
const result = domainManagerService.getWorkspaceUrls({
subdomain: 'subdomain',
customDomain: null,
isCustomDomainEnabled: false,
});
expect(result).toEqual({
customUrl: undefined,
subdomainUrl: 'https://subdomain.example.com/',
});
});
});
let domainManagerService: DomainManagerService;
describe('WorkspaceDomainsService', () => {
let workspaceDomainsService: WorkspaceDomainsService;
let twentyConfigService: TwentyConfigService;
let workspaceRepository: Repository<Workspace>;
let publicDomainRepository: Repository<PublicDomain>;
@@ -68,7 +18,8 @@ describe('DomainManagerService', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DomainManagerService,
DomainServerConfigService,
WorkspaceDomainsService,
{
provide: getRepositoryToken(Workspace),
useValue: {
@@ -97,13 +48,15 @@ describe('DomainManagerService', () => {
publicDomainRepository = module.get<Repository<PublicDomain>>(
getRepositoryToken(PublicDomain),
);
domainManagerService =
module.get<DomainManagerService>(DomainManagerService);
workspaceDomainsService = module.get<WorkspaceDomainsService>(
WorkspaceDomainsService,
);
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
});
describe('buildBaseUrl', () => {
it('should build the base URL from environment variables', () => {
describe('getWorkspaceUrls', () => {
it('should return a URL containing the correct customDomain if customDomain is provided', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
@@ -115,28 +68,41 @@ describe('DomainManagerService', () => {
return env[key];
});
const result = domainManagerService.getBaseUrl();
const result = workspaceDomainsService.getWorkspaceUrls({
subdomain: 'subdomain',
customDomain: 'custom-host.com',
isCustomDomainEnabled: true,
});
expect(result.toString()).toBe('https://example.com/');
expect(result).toEqual({
customUrl: 'https://custom-host.com/',
subdomainUrl: 'https://example.com/',
});
});
it('should append default subdomain if multiworkspace is enabled', () => {
it('should return a URL containing the correct subdomain if customDomain is not provided but subdomain is', () => {
jest
.spyOn(twentyConfigService, 'get')
.mockImplementation((key: string) => {
const env = {
FRONTEND_URL: 'https://example.com',
IS_MULTIWORKSPACE_ENABLED: true,
DEFAULT_SUBDOMAIN: 'test',
};
// @ts-expect-error legacy noImplicitAny
return env[key];
});
const result = domainManagerService.getBaseUrl();
const result = workspaceDomainsService.getWorkspaceUrls({
subdomain: 'subdomain',
customDomain: null,
isCustomDomainEnabled: false,
});
expect(result.toString()).toBe('https://test.example.com/');
expect(result).toEqual({
customUrl: undefined,
subdomainUrl: 'https://subdomain.example.com/',
});
});
});
@@ -155,7 +121,7 @@ describe('DomainManagerService', () => {
return env[key];
});
const result = domainManagerService.buildWorkspaceURL({
const result = workspaceDomainsService.buildWorkspaceURL({
workspace: {
subdomain: 'test',
customDomain: null,
@@ -178,7 +144,7 @@ describe('DomainManagerService', () => {
return env[key];
});
const result = domainManagerService.buildWorkspaceURL({
const result = workspaceDomainsService.buildWorkspaceURL({
workspace: {
subdomain: 'test',
customDomain: null,
@@ -202,7 +168,7 @@ describe('DomainManagerService', () => {
return env[key];
});
const result = domainManagerService.buildWorkspaceURL({
const result = workspaceDomainsService.buildWorkspaceURL({
workspace: {
subdomain: 'test',
customDomain: null,
@@ -240,7 +206,7 @@ describe('DomainManagerService', () => {
] as unknown as Workspace[]);
const result =
await domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
await workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
'https://example.com',
);
@@ -270,7 +236,7 @@ describe('DomainManagerService', () => {
] as unknown as Workspace[]);
const result =
await domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
await workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
'https://example.com',
);
@@ -296,7 +262,7 @@ describe('DomainManagerService', () => {
} as unknown as Promise<Workspace>);
const result =
await domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
await workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
'https://123.example.com',
);
@@ -322,7 +288,7 @@ describe('DomainManagerService', () => {
} as unknown as Promise<Workspace>);
const result =
await domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
await workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
'https://123.custom.com',
);
@@ -352,7 +318,7 @@ describe('DomainManagerService', () => {
} as unknown as Promise<PublicDomain>);
const result =
await domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
await workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
'https://123.custom.com',
);
@@ -377,7 +343,7 @@ describe('DomainManagerService', () => {
jest.spyOn(publicDomainRepository, 'findOne').mockResolvedValueOnce(null);
const result =
await domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
await workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
'https://123.custom.com',
);
@@ -4,127 +4,48 @@ import { InjectRepository } from '@nestjs/typeorm';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { type WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
import { generateRandomSubdomain } from 'src/engine/core-modules/domain-manager/utils/generate-random-subdomain';
import { getSubdomainFromEmail } from 'src/engine/core-modules/domain-manager/utils/get-subdomain-from-email';
import { getSubdomainNameFromDisplayName } from 'src/engine/core-modules/domain-manager/utils/get-subdomain-name-from-display-name';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { buildUrlWithPathnameAndSearchParams } from 'src/engine/core-modules/domain/domain-server-config/utils/build-url-with-pathname-and-search-params.util';
import { WorkspaceDomainConfig } from 'src/engine/core-modules/domain/workspace-domains/types/workspace-domain-config.type';
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
@Injectable()
export class DomainManagerService {
export class WorkspaceDomainsService {
constructor(
private readonly domainServerConfigService: DomainServerConfigService,
private readonly twentyConfigService: TwentyConfigService,
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(PublicDomain)
private readonly publicDomainRepository: Repository<PublicDomain>,
private readonly twentyConfigService: TwentyConfigService,
) {}
getFrontUrl() {
return new URL(
this.twentyConfigService.get('FRONTEND_URL') ??
this.twentyConfigService.get('SERVER_URL'),
);
}
getBaseUrl(): URL {
const baseUrl = this.getFrontUrl();
if (
this.twentyConfigService.get('IS_MULTIWORKSPACE_ENABLED') &&
this.twentyConfigService.get('DEFAULT_SUBDOMAIN')
) {
baseUrl.hostname = `${this.twentyConfigService.get('DEFAULT_SUBDOMAIN')}.${baseUrl.hostname}`;
}
return baseUrl;
}
getPublicDomainUrl(): URL {
return new URL(this.twentyConfigService.get('PUBLIC_DOMAIN_URL'));
}
private appendSearchParams(
url: URL,
searchParams: Record<string, string | number | boolean>,
) {
Object.entries(searchParams).forEach(([key, value]) => {
url.searchParams.set(key, value.toString());
});
}
buildBaseUrl({
pathname,
searchParams,
}: {
pathname?: string;
searchParams?: Record<string, string | number>;
}) {
const url = this.getBaseUrl();
if (pathname) {
url.pathname = pathname;
}
if (searchParams) {
this.appendSearchParams(url, searchParams);
}
return url;
}
buildWorkspaceURL({
workspace,
pathname,
searchParams,
}: {
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType;
workspace: WorkspaceDomainConfig;
pathname?: string;
searchParams?: Record<string, string | number | boolean>;
}) {
const workspaceUrls = this.getWorkspaceUrls(workspace);
const url = new URL(workspaceUrls.customUrl ?? workspaceUrls.subdomainUrl);
if (pathname) {
url.pathname = pathname;
}
if (searchParams) {
this.appendSearchParams(url, searchParams);
}
const url = buildUrlWithPathnameAndSearchParams({
baseUrl: new URL(workspaceUrls.customUrl ?? workspaceUrls.subdomainUrl),
pathname,
searchParams,
});
return url;
}
getSubdomainAndDomainFromUrl = (url: string) => {
const { hostname: originHostname } = new URL(url);
const frontDomain = this.getFrontUrl().hostname;
const isFrontdomain = originHostname.endsWith(`.${frontDomain}`);
const subdomain = originHostname.replace(`.${frontDomain}`, '');
return {
subdomain:
isFrontdomain && !this.isDefaultSubdomain(subdomain)
? subdomain
: undefined,
domain: isFrontdomain ? null : originHostname,
};
};
isDefaultSubdomain(subdomain: string) {
return subdomain === this.twentyConfigService.get('DEFAULT_SUBDOMAIN');
}
computeRedirectErrorUrl(
computeWorkspaceRedirectErrorUrl(
errorMessage: string,
workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType,
workspace: WorkspaceDomainConfig,
pathname: string,
) {
const url = this.buildWorkspaceURL({
@@ -168,7 +89,8 @@ export class DomainManagerService {
return this.getDefaultWorkspace();
}
const { subdomain, domain } = this.getSubdomainAndDomainFromUrl(origin);
const { subdomain, domain } =
this.domainServerConfigService.getSubdomainAndDomainFromUrl(origin);
if (!domain && !subdomain) return;
@@ -195,29 +117,8 @@ export class DomainManagerService {
return publicDomainFromCustomDomain?.workspace;
}
private extractSubdomain(params?: { email?: string; displayName?: string }) {
if (params?.email) {
return getSubdomainFromEmail(params.email);
}
if (params?.displayName) {
return getSubdomainNameFromDisplayName(params.displayName);
}
}
async generateSubdomain(params?: { email?: string; displayName?: string }) {
const subdomain =
this.extractSubdomain(params) ?? generateRandomSubdomain();
const existingWorkspaceCount = await this.workspaceRepository.countBy({
subdomain,
});
return `${subdomain}${existingWorkspaceCount > 0 ? `-${Math.random().toString(36).substring(2, 10)}` : ''}`;
}
private getCustomWorkspaceUrl(customDomain: string) {
const url = this.getFrontUrl();
const url = this.domainServerConfigService.getFrontUrl();
url.hostname = customDomain;
@@ -225,7 +126,7 @@ export class DomainManagerService {
}
private getTwentyWorkspaceUrl(subdomain: string) {
const url = this.getFrontUrl();
const url = this.domainServerConfigService.getFrontUrl();
url.hostname = this.twentyConfigService.get('IS_MULTIWORKSPACE_ENABLED')
? `${subdomain}.${url.hostname}`
@@ -235,7 +136,7 @@ export class DomainManagerService {
}
getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain(
workspace?: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType | null,
workspace?: WorkspaceDomainConfig | null,
) {
if (!workspace) {
return {
@@ -258,7 +159,7 @@ export class DomainManagerService {
subdomain,
customDomain,
isCustomDomainEnabled,
}: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType) {
}: WorkspaceDomainConfig) {
return {
customUrl:
isCustomDomainEnabled && customDomain
@@ -267,4 +168,8 @@ export class DomainManagerService {
subdomainUrl: this.getTwentyWorkspaceUrl(subdomain),
};
}
async findByCustomDomain(customDomain: string) {
return this.workspaceRepository.findOne({ where: { customDomain } });
}
}
@@ -1,6 +1,6 @@
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
export type WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType = Pick<
export type WorkspaceDomainConfig = Pick<
Workspace,
'subdomain' | 'customDomain' | 'isCustomDomainEnabled'
>;
@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Module({
imports: [
DomainServerConfigModule,
TypeOrmModule.forFeature([Workspace, PublicDomain]),
],
providers: [WorkspaceDomainsService],
exports: [WorkspaceDomainsService],
})
export class WorkspaceDomainsModule {}
@@ -3,23 +3,25 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { EmailVerificationTokenService } from 'src/engine/core-modules/auth/token/services/email-verification-token.service';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { EmailVerificationResolver } from 'src/engine/core-modules/email-verification/email-verification.resolver';
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
import { EmailModule } from 'src/engine/core-modules/email/email.module';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { User } from 'src/engine/core-modules/user/user.entity';
import { UserModule } from 'src/engine/core-modules/user/user.module';
@Module({
imports: [
TypeOrmModule.forFeature([AppToken, User]),
EmailModule,
TwentyConfigModule,
DomainManagerModule,
UserModule,
UserWorkspaceModule,
WorkspaceDomainsModule,
DomainServerConfigModule,
],
providers: [
EmailVerificationService,
@@ -1,7 +1,7 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { ResendEmailVerificationTokenInput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.input';
import { ResendEmailVerificationTokenOutput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.output';
import { EmailVerificationExceptionFilter } from 'src/engine/core-modules/email-verification/email-verification-exception-filter.util';
@@ -20,7 +20,7 @@ import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
export class EmailVerificationResolver {
constructor(
private readonly emailVerificationService: EmailVerificationService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
) {}
// TODO: this should be an authenticated endpoint
@@ -33,7 +33,7 @@ export class EmailVerificationResolver {
@Context() context: I18nContext,
): Promise<ResendEmailVerificationTokenOutput> {
const workspace =
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
origin,
);
@@ -16,8 +16,9 @@ import {
AppTokenType,
} from 'src/engine/core-modules/app-token/app-token.entity';
import { EmailVerificationTokenService } from 'src/engine/core-modules/auth/token/services/email-verification-token.service';
import { type WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { WorkspaceDomainConfig } from 'src/engine/core-modules/domain/workspace-domains/types/workspace-domain-config.type';
import {
EmailVerificationException,
EmailVerificationExceptionCode,
@@ -32,7 +33,8 @@ export class EmailVerificationService {
constructor(
@InjectRepository(AppToken)
private readonly appTokenRepository: Repository<AppToken>,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly domainsServerConfigService: DomainServerConfigService,
private readonly emailService: EmailService,
private readonly twentyConfigService: TwentyConfigService,
private readonly userService: UserService,
@@ -43,9 +45,7 @@ export class EmailVerificationService {
async sendVerificationEmail(
userId: string,
email: string,
workspace:
| WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType
| undefined,
workspace: WorkspaceDomainConfig | undefined,
locale: keyof typeof APP_LOCALES,
verifyEmailRedirectPath?: string,
) {
@@ -67,11 +67,13 @@ export class EmailVerificationService {
},
};
const verificationLink = workspace
? this.domainManagerService.buildWorkspaceURL({
? this.workspaceDomainsService.buildWorkspaceURL({
workspace,
...linkPathnameAndSearchParams,
})
: this.domainManagerService.buildBaseUrl(linkPathnameAndSearchParams);
: this.domainsServerConfigService.buildBaseUrl(
linkPathnameAndSearchParams,
);
const emailData = {
link: verificationLink.toString(),
@@ -104,9 +106,7 @@ export class EmailVerificationService {
async resendEmailVerificationToken(
email: string,
workspace:
| WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType
| undefined,
workspace: WorkspaceDomainConfig | undefined,
locale: keyof typeof APP_LOCALES,
) {
if (!this.twentyConfigService.get('IS_EMAIL_VERIFICATION_REQUIRED')) {
@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
@Module({
imports: [DomainManagerModule],
imports: [WorkspaceDomainsModule, DomainServerConfigModule],
providers: [GuardRedirectService],
exports: [GuardRedirectService],
})
@@ -1,4 +1,4 @@
import { type ExecutionContext, Injectable } from '@nestjs/common';
import { ExecutionContext, Injectable } from '@nestjs/common';
import { type Request } from 'express';
import { AppPath } from 'twenty-shared/types';
@@ -7,7 +7,8 @@ import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { type CustomException } from 'src/utils/custom-exception';
@@ -15,9 +16,10 @@ import { type CustomException } from 'src/utils/custom-exception';
@Injectable()
export class GuardRedirectService {
constructor(
private readonly domainManagerService: DomainManagerService,
private readonly twentyConfigService: TwentyConfigService,
private readonly exceptionHandlerService: ExceptionHandlerService,
private readonly domainsServerConfigService: DomainServerConfigService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
) {}
dispatchErrorFromGuard(
@@ -48,7 +50,7 @@ export class GuardRedirectService {
const request = context.switchToHttp().getRequest<Request>();
const subdomainAndDomainFromReferer = request.headers.referer
? this.domainManagerService.getSubdomainAndDomainFromUrl(
? this.domainsServerConfigService.getSubdomainAndDomainFromUrl(
request.headers.referer,
)
: null;
@@ -95,7 +97,7 @@ export class GuardRedirectService {
}) {
this.captureException(error, workspace.id);
return this.domainManagerService.computeRedirectErrorUrl(
return this.workspaceDomainsService.computeWorkspaceRedirectErrorUrl(
error instanceof AuthException ? error.message : 'Unknown error',
{
subdomain: workspace.subdomain,
@@ -9,7 +9,7 @@ import {
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { ImpersonationService } from 'src/engine/core-modules/impersonation/services/impersonation.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { OTPStatus } from 'src/engine/core-modules/two-factor-authentication/strategies/otp/otp.constants';
@@ -61,7 +61,7 @@ describe('ImpersonationService', () => {
},
},
{
provide: DomainManagerService,
provide: WorkspaceDomainsService,
useValue: {
getWorkspaceUrls: jest.fn().mockReturnValue({
customUrl: undefined,
@@ -3,7 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { ImpersonationResolver } from 'src/engine/core-modules/impersonation/impersonation.resolver';
import { ImpersonationService } from 'src/engine/core-modules/impersonation/services/impersonation.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
@@ -23,7 +23,7 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.
UserRoleModule,
AuditModule,
TypeOrmModule.forFeature([UserWorkspace, Workspace, User]),
DomainManagerModule,
WorkspaceDomainsModule,
PermissionsModule,
],
providers: [ImpersonationService, ImpersonationResolver],
@@ -13,7 +13,7 @@ import {
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { twoFactorAuthenticationMethodsValidator } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication.validation';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
@@ -25,7 +25,7 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm
export class ImpersonationService {
constructor(
private readonly auditService: AuditService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly loginTokenService: LoginTokenService,
private readonly twentyConfigService: TwentyConfigService,
@InjectRepository(UserWorkspace)
@@ -172,7 +172,7 @@ export class ImpersonationService {
return {
workspace: {
id: toImpersonateUserWorkspace.workspace.id,
workspaceUrls: this.domainManagerService.getWorkspaceUrls(
workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls(
toImpersonateUserWorkspace.workspace,
),
},
@@ -6,7 +6,6 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module';
@@ -24,7 +23,6 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
FeatureFlag,
]),
BillingModule,
DomainManagerModule,
GuardRedirectModule,
PermissionsModule,
FeatureFlagModule,
@@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
@@ -13,13 +13,13 @@ import { UserModule } from 'src/engine/core-modules/user/user.module';
import { TwoFactorAuthenticationResolver } from './two-factor-authentication.resolver';
import { TwoFactorAuthenticationService } from './two-factor-authentication.service';
import { SimpleSecretEncryptionUtil } from './utils/simple-secret-encryption.util';
import { TwoFactorAuthenticationMethod } from './entities/two-factor-authentication-method.entity';
import { SimpleSecretEncryptionUtil } from './utils/simple-secret-encryption.util';
@Module({
imports: [
UserWorkspaceModule,
DomainManagerModule,
WorkspaceDomainsModule,
MetricsModule,
TokenModule,
JwtModule,
@@ -6,7 +6,7 @@ import {
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { type User } from 'src/engine/core-modules/user/user.entity';
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -37,7 +37,7 @@ const createMockUserService = () => ({
findUserByEmailOrThrow: jest.fn(),
});
const createMockDomainManagerService = () => ({
const createMockWorkspaceDomainsService = () => ({
getWorkspaceByOriginOrDefaultWorkspace: jest.fn(),
});
@@ -48,7 +48,9 @@ describe('TwoFactorAuthenticationResolver', () => {
>;
let loginTokenService: ReturnType<typeof createMockLoginTokenService>;
let userService: ReturnType<typeof createMockUserService>;
let domainManagerService: ReturnType<typeof createMockDomainManagerService>;
let workspaceDomainsService: ReturnType<
typeof createMockWorkspaceDomainsService
>;
let repository: ReturnType<typeof createMockRepository>;
const mockUser: User = {
@@ -86,8 +88,8 @@ describe('TwoFactorAuthenticationResolver', () => {
useFactory: createMockUserService,
},
{
provide: DomainManagerService,
useFactory: createMockDomainManagerService,
provide: WorkspaceDomainsService,
useFactory: createMockWorkspaceDomainsService,
},
{
provide: getRepositoryToken(TwoFactorAuthenticationMethod),
@@ -102,7 +104,7 @@ describe('TwoFactorAuthenticationResolver', () => {
twoFactorAuthenticationService = module.get(TwoFactorAuthenticationService);
loginTokenService = module.get(LoginTokenService);
userService = module.get(UserService);
domainManagerService = module.get(DomainManagerService);
workspaceDomainsService = module.get(WorkspaceDomainsService);
repository = module.get(getRepositoryToken(TwoFactorAuthenticationMethod));
});
@@ -125,7 +127,7 @@ describe('TwoFactorAuthenticationResolver', () => {
sub: mockUser.email,
workspaceId: mockWorkspace.id,
});
domainManagerService.getWorkspaceByOriginOrDefaultWorkspace.mockResolvedValue(
workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace.mockResolvedValue(
mockWorkspace,
);
userService.findUserByEmailOrThrow.mockResolvedValue(mockUser);
@@ -144,7 +146,7 @@ describe('TwoFactorAuthenticationResolver', () => {
mockInput.loginToken,
);
expect(
domainManagerService.getWorkspaceByOriginOrDefaultWorkspace,
workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace,
).toHaveBeenCalledWith(origin);
expect(userService.findUserByEmailOrThrow).toHaveBeenCalledWith(
mockUser.email,
@@ -160,7 +162,7 @@ describe('TwoFactorAuthenticationResolver', () => {
});
it('should throw WORKSPACE_NOT_FOUND when workspace is not found', async () => {
domainManagerService.getWorkspaceByOriginOrDefaultWorkspace.mockResolvedValue(
workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace.mockResolvedValue(
null,
);
@@ -11,7 +11,7 @@ import {
} from 'src/engine/core-modules/auth/auth.exception';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -39,7 +39,7 @@ export class TwoFactorAuthenticationResolver {
private readonly twoFactorAuthenticationService: TwoFactorAuthenticationService,
private readonly loginTokenService: LoginTokenService,
private readonly userService: UserService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
@InjectRepository(TwoFactorAuthenticationMethod)
private readonly twoFactorAuthenticationMethodRepository: Repository<TwoFactorAuthenticationMethod>,
) {}
@@ -57,7 +57,7 @@ export class TwoFactorAuthenticationResolver {
);
const workspace =
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
origin,
);
@@ -6,7 +6,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { ApprovedAccessDomainModule } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
@@ -31,7 +31,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
WorkspaceDataSourceModule,
ApprovedAccessDomainModule,
WorkspaceInvitationModule,
DomainManagerModule,
WorkspaceDomainsModule,
TwentyORMModule,
UserRoleModule,
FileUploadModule,
@@ -9,7 +9,7 @@ import { type ApprovedAccessDomain } from 'src/engine/core-modules/approved-acce
import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service';
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import {
FileUploadService,
@@ -81,7 +81,7 @@ describe('UserWorkspaceService', () => {
},
},
{
provide: DomainManagerService,
provide: WorkspaceDomainsService,
useValue: {
getWorkspaceUrls: jest.fn(),
},
@@ -16,7 +16,7 @@ import {
} from 'src/engine/core-modules/auth/auth.exception';
import { type AvailableWorkspace } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
@@ -43,7 +43,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly workspaceInvitationService: WorkspaceInvitationService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly loginTokenService: LoginTokenService,
private readonly approvedAccessDomainService: ApprovedAccessDomainService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@@ -384,7 +384,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
return {
id: workspace.id,
displayName: workspace.displayName,
workspaceUrls: this.domainManagerService.getWorkspaceUrls(workspace),
workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls(workspace),
logo: workspace.logo
? this.fileService.signFileUrl({
url: workspace.logo,
@@ -7,7 +7,7 @@ import {
AppToken,
AppTokenType,
} from 'src/engine/core-modules/app-token/app-token.entity';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
@@ -60,7 +60,7 @@ describe('WorkspaceInvitationService', () => {
useClass: Repository,
},
{
provide: DomainManagerService,
provide: WorkspaceDomainsService,
useValue: {
buildWorkspaceURL: jest
.fn()
@@ -20,7 +20,7 @@ import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
@@ -46,7 +46,7 @@ export class WorkspaceInvitationService {
private readonly twentyConfigService: TwentyConfigService,
private readonly emailService: EmailService,
private readonly onboardingService: OnboardingService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly i18nService: I18nService,
private readonly fileService: FileService,
) {}
@@ -281,7 +281,7 @@ export class WorkspaceInvitationService {
for (const invitation of invitationsPr) {
if (invitation.status === 'fulfilled') {
const link = this.domainManagerService.buildWorkspaceURL({
const link = this.workspaceDomainsService.buildWorkspaceURL({
workspace,
pathname: getAppPath(AppPath.Invite, {
workspaceInviteHash: workspace?.inviteHash,
@@ -3,7 +3,7 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
@@ -15,7 +15,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
@Module({
imports: [
DomainManagerModule,
WorkspaceDomainsModule,
NestjsQueryTypeOrmModule.forFeature([AppToken, UserWorkspace, Workspace]),
FileModule,
OnboardingModule,
@@ -0,0 +1,122 @@
export const RESERVED_SUBDOMAINS = [
'trust',
'demo',
'api',
't',
'companies',
'telemetry',
'logs',
'metrics',
'next',
'main',
'admin',
'dashboard',
'dash',
'billing',
'db',
'favicon',
'www',
'mail',
'docs',
'dev',
'app',
'staging',
'production',
'developer',
'files',
'cdn',
'storage',
'about',
'help',
'support',
'contact',
'privacy',
'terms',
'careers',
'jobs',
'blog',
'news',
'events',
'community',
'forum',
'chat',
'test',
'testing',
'feedback',
'config',
'settings',
'media',
'image',
'audio',
'video',
'images',
'partners',
'partnership',
'partnerships',
'assets',
'login',
'signin',
'signup',
'legal',
'shop',
'merch',
'store',
'auth',
'register',
'payment',
'fr',
'de',
'it',
'es',
'pt',
'nl',
'be',
'ch',
'us',
'ca',
'au',
'nz',
'za',
'eu',
'uk',
'ru',
'ua',
'pl',
'ro',
'bg',
'gr',
'cz',
'sk',
'hu',
'hr',
'si',
'rs',
'me',
'ba',
'mk',
'al',
'az',
'tr',
'cy',
'lv',
'lt',
'ee',
'fi',
'is',
'no',
'se',
'dk',
'asia',
'africa',
'america',
'europe',
'north-america',
'south-africa',
'north-africa',
'south-america',
'oceania',
'paris',
'london',
'new-york',
'san-francisco',
];
@@ -0,0 +1,2 @@
export const VALID_SUBDOMAIN_PATTERN =
/^(?!api-).*^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/;
@@ -1,14 +1,14 @@
import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, Not, Repository, Raw } from 'typeorm';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { IsNull, Not, Raw, Repository } from 'typeorm';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { CustomDomainManagerService } from 'src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
export const CHECK_CUSTOM_DOMAIN_VALID_RECORDS_CRON_PATTERN = '0 * * * *';
@@ -17,7 +17,7 @@ export class CheckCustomDomainValidRecordsCronJob {
constructor(
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
private readonly workspaceService: WorkspaceService,
private readonly customDomainManagerService: CustomDomainManagerService,
) {}
@Process(CheckCustomDomainValidRecordsCronJob.name)
@@ -39,7 +39,9 @@ export class CheckCustomDomainValidRecordsCronJob {
for (const workspace of workspaces) {
try {
await this.workspaceService.checkCustomDomainValidRecords(workspace);
await this.customDomainManagerService.checkCustomDomainValidRecords(
workspace,
);
} catch (error) {
throw new Error(
`[${CheckCustomDomainValidRecordsCronJob.name}] Cannot check custom domain for workspaces: ${error.message}`,
@@ -3,7 +3,6 @@ import { Field, InputType } from '@nestjs/graphql';
import {
IsBoolean,
IsInt,
IsNotIn,
IsOptional,
IsString,
IsUUID,
@@ -18,129 +17,6 @@ export class UpdateWorkspaceInput {
@Field({ nullable: true })
@IsString()
@IsOptional()
@Matches(/^(?!api-).*^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/)
@IsNotIn([
'trust',
'demo',
'api',
't',
'companies',
'telemetry',
'logs',
'metrics',
'next',
'main',
'admin',
'dashboard',
'dash',
'billing',
'db',
'favicon',
'www',
'mail',
'docs',
'dev',
'app',
'staging',
'production',
'developer',
'files',
'cdn',
'storage',
'about',
'help',
'support',
'contact',
'privacy',
'terms',
'careers',
'jobs',
'blog',
'news',
'events',
'community',
'forum',
'chat',
'test',
'testing',
'feedback',
'config',
'settings',
'media',
'image',
'audio',
'video',
'images',
'partners',
'partnership',
'partnerships',
'assets',
'login',
'signin',
'signup',
'legal',
'shop',
'merch',
'store',
'auth',
'register',
'payment',
'fr',
'de',
'it',
'es',
'pt',
'nl',
'be',
'ch',
'us',
'ca',
'au',
'nz',
'za',
'eu',
'uk',
'ru',
'ua',
'pl',
'ro',
'bg',
'gr',
'cz',
'sk',
'hu',
'hr',
'si',
'rs',
'me',
'ba',
'mk',
'al',
'az',
'tr',
'cy',
'lv',
'lt',
'ee',
'fi',
'is',
'no',
'se',
'dk',
'asia',
'africa',
'america',
'europe',
'north-america',
'south-africa',
'north-africa',
'south-america',
'oceania',
'paris',
'london',
'new-york',
'san-francisco',
])
subdomain?: string;
@Field({ nullable: true })
@@ -7,7 +7,8 @@ import { AuditService } from 'src/engine/core-modules/audit/services/audit.servi
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { CustomDomainManagerService } from 'src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service';
import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service';
import { EmailService } from 'src/engine/core-modules/email/email.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@@ -93,8 +94,9 @@ describe('WorkspaceService', () => {
WorkspaceManagerService,
UserWorkspaceService,
UserService,
DomainManagerService,
DnsManagerService,
CustomDomainManagerService,
SubdomainManagerService,
TwentyConfigService,
EmailService,
OnboardingService,
@@ -9,14 +9,12 @@ import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { Repository } from 'typeorm';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated';
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records';
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
import { CustomDomainManagerService } from 'src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service';
import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import {
@@ -26,7 +24,6 @@ import {
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
@@ -60,8 +57,6 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
constructor(
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(PublicDomain)
private readonly publicDomainRepository: Repository<PublicDomain>,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@InjectRepository(UserWorkspace)
@@ -77,84 +72,14 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
private readonly dnsManagerService: DnsManagerService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly auditService: AuditService,
private readonly subdomainManagerService: SubdomainManagerService,
private readonly customDomainManagerService: CustomDomainManagerService,
@InjectMessageQueue(MessageQueue.deleteCascadeQueue)
private readonly messageQueueService: MessageQueueService,
) {
super(workspaceRepository);
}
private async isCustomDomainEnabled(workspaceId: string) {
const isCustomDomainBillingEnabled =
await this.billingService.hasEntitlement(
workspaceId,
this.featureLookUpKey,
);
if (!isCustomDomainBillingEnabled) {
throw new WorkspaceException(
`No entitlement found for this workspace`,
WorkspaceExceptionCode.WORKSPACE_CUSTOM_DOMAIN_DISABLED,
);
}
}
private async validateSubdomainUpdate(newSubdomain: string) {
const subdomainAvailable = await this.isSubdomainAvailable(newSubdomain);
if (
!subdomainAvailable ||
this.twentyConfigService.get('DEFAULT_SUBDOMAIN') === newSubdomain
) {
throw new WorkspaceException(
'Subdomain already taken',
WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN,
);
}
}
private async setCustomDomain(workspace: Workspace, customDomain: string) {
await this.isCustomDomainEnabled(workspace.id);
const existingWorkspace = await this.workspaceRepository.findOne({
where: { customDomain },
});
if (existingWorkspace && existingWorkspace.id !== workspace.id) {
throw new WorkspaceException(
'Domain already taken',
WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN,
);
}
if (
await this.publicDomainRepository.findOneBy({
domain: customDomain,
})
) {
throw new WorkspaceException(
'Domain is already registered as public domain',
WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN,
{
userFriendlyMessage: msg`Domain is already registered as public domain`,
},
);
}
if (!isDefined(customDomain) || workspace.customDomain === customDomain) {
return;
}
if (isDefined(workspace.customDomain)) {
await this.dnsManagerService.updateHostname(
workspace.customDomain,
customDomain,
);
} else {
await this.dnsManagerService.registerHostname(customDomain);
}
}
async updateWorkspaceById({
payload,
userWorkspaceId,
@@ -186,7 +111,9 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
});
if (payload.subdomain && workspace.subdomain !== payload.subdomain) {
await this.validateSubdomainUpdate(payload.subdomain);
await this.subdomainManagerService.validateSubdomainOrThrow(
payload.subdomain,
);
}
let customDomainRegistered = false;
@@ -202,7 +129,10 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
payload.customDomain &&
workspace.customDomain !== payload.customDomain
) {
await this.setCustomDomain(workspace, payload.customDomain);
await this.customDomainManagerService.setCustomDomain(
workspace,
payload.customDomain,
);
customDomainRegistered = true;
}
@@ -410,14 +340,6 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
}
}
async isSubdomainAvailable(subdomain: string) {
const existingWorkspace = await this.workspaceRepository.findOne({
where: { subdomain: subdomain },
});
return !existingWorkspace;
}
private async validateSecurityPermissions({
payload,
userWorkspaceId,
@@ -508,45 +430,4 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
}
}
}
async checkCustomDomainValidRecords(
workspace: Workspace,
domainValidRecord?: DomainValidRecords,
) {
assertIsDefinedOrThrow(workspace.customDomain);
const customDomainWithRecords =
domainValidRecord ??
(await this.dnsManagerService.getHostnameWithRecords(
workspace.customDomain,
));
assertIsDefinedOrThrow(customDomainWithRecords);
const isCustomDomainWorking =
await this.dnsManagerService.isHostnameWorking(workspace.customDomain);
if (workspace.isCustomDomainEnabled !== isCustomDomainWorking) {
workspace.isCustomDomainEnabled = isCustomDomainWorking;
await this.workspaceRepository.save(workspace);
const analytics = this.auditService.createContext({
workspaceId: workspace.id,
});
analytics.insertWorkspaceEvent(
workspace.isCustomDomainEnabled
? CUSTOM_DOMAIN_ACTIVATED_EVENT
: CUSTOM_DOMAIN_DEACTIVATED_EVENT,
{},
);
}
return customDomainWithRecords;
}
async findByCustomDomain(customDomain: string) {
return this.workspaceRepository.findOne({ where: { customDomain } });
}
}
@@ -19,6 +19,7 @@ export const workspaceGraphqlApiExceptionHandler = (error: Error) => {
throw new NotFoundError(error);
case WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN:
case WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN:
case WorkspaceExceptionCode.SUBDOMAIN_NOT_VALID:
throw new ConflictError(error);
case WorkspaceExceptionCode.ENVIRONMENT_VAR_NOT_ENABLED:
case WorkspaceExceptionCode.WORKSPACE_CUSTOM_DOMAIN_DISABLED:
@@ -5,6 +5,7 @@ export class WorkspaceException extends CustomException<WorkspaceExceptionCode>
export enum WorkspaceExceptionCode {
SUBDOMAIN_NOT_FOUND = 'SUBDOMAIN_NOT_FOUND',
SUBDOMAIN_ALREADY_TAKEN = 'SUBDOMAIN_ALREADY_TAKEN',
SUBDOMAIN_NOT_VALID = 'SUBDOMAIN_NOT_VALID',
DOMAIN_ALREADY_TAKEN = 'DOMAIN_ALREADY_TAKEN',
WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND',
WORKSPACE_CUSTOM_DOMAIN_DISABLED = 'WORKSPACE_CUSTOM_DOMAIN_DISABLED',
@@ -10,7 +10,9 @@ import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { DnsManagerModule } from 'src/engine/core-modules/dns-manager/dns-manager.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { CustomDomainManagerModule } from 'src/engine/core-modules/domain/custom-domain-manager/custom-domain-manager.module';
import { SubdomainManagerModule } from 'src/engine/core-modules/domain/subdomain-manager/subdomain-manager.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
@@ -65,7 +67,9 @@ import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-m
RoleModule,
AgentModule,
DnsManagerModule,
DomainManagerModule,
WorkspaceDomainsModule,
SubdomainManagerModule,
CustomDomainManagerModule,
ViewModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
@@ -25,7 +25,8 @@ import { BillingSubscription } from 'src/engine/core-modules/billing/entities/bi
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records';
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { CustomDomainManagerService } from 'src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { FeatureFlagDTO } from 'src/engine/core-modules/feature-flag/dtos/feature-flag-dto';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@@ -89,7 +90,7 @@ const OriginHeader = createParamDecorator(
export class WorkspaceResolver {
constructor(
private readonly workspaceService: WorkspaceService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly twentyConfigService: TwentyConfigService,
private readonly fileUploadService: FileUploadService,
@@ -100,6 +101,7 @@ export class WorkspaceResolver {
private readonly agentService: AgentService,
private readonly viewService: ViewService,
private readonly dnsManagerService: DnsManagerService,
private readonly customDomainManagerService: CustomDomainManagerService,
) {}
@Query(() => Workspace)
@@ -295,7 +297,7 @@ export class WorkspaceResolver {
@ResolveField(() => WorkspaceUrlsDTO)
workspaceUrls(@Parent() workspace: Workspace) {
return this.domainManagerService.getWorkspaceUrls(workspace);
return this.workspaceDomainsService.getWorkspaceUrls(workspace);
}
@ResolveField(() => Boolean)
@@ -356,7 +358,7 @@ export class WorkspaceResolver {
}
const workspace =
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
origin,
);
@@ -379,7 +381,7 @@ export class WorkspaceResolver {
id: workspace.id,
logo: workspaceLogoWithToken,
displayName: workspace.displayName,
workspaceUrls: this.domainManagerService.getWorkspaceUrls(workspace),
workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls(workspace),
authProviders: getAuthProvidersByWorkspace({
workspace,
systemEnabledProviders,
@@ -407,7 +409,7 @@ export class WorkspaceResolver {
workspace.customDomain,
);
return this.workspaceService.checkCustomDomainValidRecords(
return this.customDomainManagerService.checkCustomDomainValidRecords(
workspace,
domainValidRecords,
);
@@ -17,7 +17,7 @@ import { In } from 'typeorm';
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentHandoffToolService } from 'src/engine/metadata-modules/agent/agent-handoff-tool.service';
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
@@ -47,7 +47,7 @@ export class AgentExecutionService implements AgentExecutionContext {
constructor(
private readonly agentHandoffToolService: AgentHandoffToolService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly aiModelRegistryService: AiModelRegistryService,
@@ -218,7 +218,7 @@ export class AgentExecutionService implements AgentExecutionContext {
).map((record) => {
return {
...record,
resourceUrl: this.domainManagerService.buildWorkspaceURL({
resourceUrl: this.workspaceDomainsService.buildWorkspaceURL({
workspace,
pathname: getAppPath(AppPath.RecordShowPage, {
objectNameSingular:
@@ -4,7 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AiModule } from 'src/engine/core-modules/ai/ai.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
@@ -68,7 +68,7 @@ import { AgentActorContextService } from './services/agent-actor-context.service
WorkspacePermissionsCacheModule,
WorkspaceCacheStorageModule,
TokenModule,
DomainManagerModule,
WorkspaceDomainsModule,
WorkflowToolsModule,
UserWorkspaceModule,
UserRoleModule,
@@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { RouteTriggerResolver } from 'src/engine/metadata-modules/route-trigger/resolvers/route-trigger.resolver';
import { RouteTriggerController } from 'src/engine/metadata-modules/route-trigger/route-trigger.controller';
@@ -17,7 +17,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
imports: [
TypeOrmModule.forFeature([RouteTrigger]),
AuthModule,
DomainManagerModule,
WorkspaceDomainsModule,
ServerlessFunctionModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationV2Module,
@@ -1,29 +1,29 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Request } from 'express';
import { match } from 'path-to-regexp';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import {
HTTPMethod,
RouteTrigger,
} from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import {
RouteTriggerException,
RouteTriggerExceptionCode,
} from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception';
import {
HTTPMethod,
RouteTrigger,
} from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
@Injectable()
export class RouteTriggerService {
constructor(
private readonly accessTokenService: AccessTokenService,
private readonly serverlessFunctionService: ServerlessFunctionService,
private readonly domainManagerService: DomainManagerService,
private readonly workspaceDomainsService: WorkspaceDomainsService,
@InjectRepository(RouteTrigger)
private readonly routeTriggerRepository: Repository<RouteTrigger>,
) {}
@@ -41,7 +41,7 @@ export class RouteTriggerService {
const host = `${request.protocol}://${request.get('host')}`;
const workspace =
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace(
host,
);