diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e1345956e..ab082c3d2 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -81,7 +81,6 @@ module.exports = { '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-empty-interface': [ 'error', { diff --git a/packages/twenty-front/codegen-metadata.cjs b/packages/twenty-front/codegen-metadata.cjs index e9ddf7575..53429715c 100644 --- a/packages/twenty-front/codegen-metadata.cjs +++ b/packages/twenty-front/codegen-metadata.cjs @@ -18,7 +18,7 @@ module.exports = { }, config: { namingConvention: { enumValues: 'keep' }, - } + }, }, }, }; diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index cd728ef0e..940cd907f 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -113,7 +113,7 @@ export type AvailableWorkspaceOutput = { id: Scalars['String']['output']; logo?: Maybe; sso: Array; - subdomain: Scalars['String']['output']; + workspaceUrls: WorkspaceUrls; }; export type Billing = { @@ -475,19 +475,11 @@ export type EnvironmentVariable = { }; export enum EnvironmentVariablesGroup { - Analytics = 'Analytics', Authentication = 'Authentication', - Billing = 'Billing', - Cache = 'Cache', - Database = 'Database', Email = 'Email', - Frontend = 'Frontend', Logging = 'Logging', Other = 'Other', - QueueConfig = 'QueueConfig', ServerConfig = 'ServerConfig', - Storage = 'Storage', - Support = 'Support', Workspace = 'Workspace' } @@ -706,7 +698,7 @@ export enum IdentityProviderType { export type ImpersonateOutput = { __typename?: 'ImpersonateOutput'; loginToken: AuthToken; - workspace: WorkspaceSubdomainAndId; + workspace: WorkspaceUrlsAndId; }; export type Index = { @@ -1360,10 +1352,9 @@ export type PublicWorkspaceDataOutput = { __typename?: 'PublicWorkspaceDataOutput'; authProviders: AuthProviders; displayName?: Maybe; - hostname?: Maybe; id: Scalars['String']['output']; logo?: Maybe; - subdomain: Scalars['String']['output']; + workspaceUrls: WorkspaceUrls; }; export type PublishServerlessFunctionInput = { @@ -1394,7 +1385,7 @@ export type Query = { getHostnameDetails?: Maybe; getPostgresCredentials?: Maybe; getProductPrices: BillingProductPricesOutput; - getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput; + getPublicWorkspaceDataByDomain: PublicWorkspaceDataOutput; getRoles: Array; getServerlessFunctionSourceCode?: Maybe; getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal; @@ -1792,7 +1783,7 @@ export type SetupSsoOutput = { export type SignUpOutput = { __typename?: 'SignUpOutput'; loginToken: AuthToken; - workspace: WorkspaceSubdomainAndId; + workspace: WorkspaceUrlsAndId; }; export enum SubscriptionInterval { @@ -1994,7 +1985,7 @@ export type User = { analyticsTinybirdJwts?: Maybe; canImpersonate: Scalars['Boolean']['output']; createdAt: Scalars['DateTime']['output']; - currentWorkspace?: Maybe; + currentWorkspace: Workspace; defaultAvatarUrl?: Maybe; deletedAt?: Maybe; disabled?: Maybe; @@ -2126,6 +2117,7 @@ export type Workspace = { subdomain: Scalars['String']['output']; updatedAt: Scalars['DateTime']['output']; workspaceMembersCount?: Maybe; + workspaceUrls: WorkspaceUrls; }; export enum WorkspaceActivationStatus { @@ -2202,10 +2194,16 @@ export type WorkspaceNameAndId = { id: Scalars['String']['output']; }; -export type WorkspaceSubdomainAndId = { - __typename?: 'WorkspaceSubdomainAndId'; +export type WorkspaceUrls = { + __typename?: 'workspaceUrls'; + customUrl?: Maybe; + subdomainUrl: Scalars['String']['output']; +}; + +export type WorkspaceUrlsAndId = { + __typename?: 'workspaceUrlsAndId'; id: Scalars['String']['output']; - subdomain: Scalars['String']['output']; + workspaceUrls: WorkspaceUrls; }; export type RemoteServerFieldsFragment = { __typename?: 'RemoteServer', id: string, createdAt: any, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: any, schema?: string | null, label: string, userMappingOptions?: { __typename?: 'UserMappingOptionsUser', user?: string | null } | null }; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 253bd54f4..d8b11e3be 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -106,7 +106,7 @@ export type AvailableWorkspaceOutput = { id: Scalars['String']; logo?: Maybe; sso: Array; - subdomain: Scalars['String']; + workspaceUrls: WorkspaceUrls; }; export type Billing = { @@ -407,19 +407,11 @@ export type EnvironmentVariable = { }; export enum EnvironmentVariablesGroup { - Analytics = 'Analytics', Authentication = 'Authentication', - Billing = 'Billing', - Cache = 'Cache', - Database = 'Database', Email = 'Email', - Frontend = 'Frontend', Logging = 'Logging', Other = 'Other', - QueueConfig = 'QueueConfig', ServerConfig = 'ServerConfig', - Storage = 'Storage', - Support = 'Support', Workspace = 'Workspace' } @@ -631,7 +623,7 @@ export enum IdentityProviderType { export type ImpersonateOutput = { __typename?: 'ImpersonateOutput'; loginToken: AuthToken; - workspace: WorkspaceSubdomainAndId; + workspace: WorkspaceUrlsAndId; }; export type Index = { @@ -1227,10 +1219,9 @@ export type PublicWorkspaceDataOutput = { __typename?: 'PublicWorkspaceDataOutput'; authProviders: AuthProviders; displayName?: Maybe; - hostname?: Maybe; id: Scalars['String']; logo?: Maybe; - subdomain: Scalars['String']; + workspaceUrls: WorkspaceUrls; }; export type PublishServerlessFunctionInput = { @@ -1258,7 +1249,7 @@ export type Query = { getHostnameDetails?: Maybe; getPostgresCredentials?: Maybe; getProductPrices: BillingProductPricesOutput; - getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput; + getPublicWorkspaceDataByDomain: PublicWorkspaceDataOutput; getRoles: Array; getServerlessFunctionSourceCode?: Maybe; getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal; @@ -1588,7 +1579,7 @@ export type SetupSsoOutput = { export type SignUpOutput = { __typename?: 'SignUpOutput'; loginToken: AuthToken; - workspace: WorkspaceSubdomainAndId; + workspace: WorkspaceUrlsAndId; }; export enum SubscriptionInterval { @@ -1782,7 +1773,7 @@ export type User = { analyticsTinybirdJwts?: Maybe; canImpersonate: Scalars['Boolean']; createdAt: Scalars['DateTime']; - currentWorkspace?: Maybe; + currentWorkspace: Workspace; defaultAvatarUrl?: Maybe; deletedAt?: Maybe; disabled?: Maybe; @@ -1904,6 +1895,7 @@ export type Workspace = { subdomain: Scalars['String']; updatedAt: Scalars['DateTime']; workspaceMembersCount?: Maybe; + workspaceUrls: WorkspaceUrls; }; export enum WorkspaceActivationStatus { @@ -1980,10 +1972,16 @@ export type WorkspaceNameAndId = { id: Scalars['String']; }; -export type WorkspaceSubdomainAndId = { - __typename?: 'WorkspaceSubdomainAndId'; +export type WorkspaceUrls = { + __typename?: 'workspaceUrls'; + customUrl?: Maybe; + subdomainUrl: Scalars['String']; +}; + +export type WorkspaceUrlsAndId = { + __typename?: 'workspaceUrlsAndId'; id: Scalars['String']; - subdomain: Scalars['String']; + workspaceUrls: WorkspaceUrls; }; export type TimelineCalendarEventFragmentFragment = { __typename?: 'TimelineCalendarEvent', id: any, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: CalendarChannelVisibility, participants: Array<{ __typename?: 'TimelineCalendarEventParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }; @@ -2130,7 +2128,7 @@ export type ImpersonateMutationVariables = Exact<{ }>; -export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'ImpersonateOutput', workspace: { __typename?: 'WorkspaceSubdomainAndId', subdomain: string, id: string }, loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } }; +export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'ImpersonateOutput', workspace: { __typename?: 'workspaceUrlsAndId', id: string, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } }, loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } }; export type RenewTokenMutationVariables = Exact<{ appToken: Scalars['String']; @@ -2156,7 +2154,7 @@ export type SignUpMutationVariables = Exact<{ }>; -export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceSubdomainAndId', id: string, subdomain: string } } }; +export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'workspaceUrlsAndId', id: string, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } } }; export type UpdatePasswordViaResetTokenMutationVariables = Exact<{ token: Scalars['String']; @@ -2172,12 +2170,12 @@ export type CheckUserExistsQueryVariables = Exact<{ }>; -export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename: 'UserExists', exists: boolean, isEmailVerified: boolean, availableWorkspaces: Array<{ __typename?: 'AvailableWorkspaceOutput', id: string, displayName?: string | null, subdomain: string, hostname?: string | null, logo?: string | null, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } | { __typename: 'UserNotExists', exists: boolean } }; +export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename: 'UserExists', exists: boolean, isEmailVerified: boolean, availableWorkspaces: Array<{ __typename?: 'AvailableWorkspaceOutput', id: string, displayName?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } | { __typename: 'UserNotExists', exists: boolean } }; -export type GetPublicWorkspaceDataBySubdomainQueryVariables = Exact<{ [key: string]: never; }>; +export type GetPublicWorkspaceDataByDomainQueryVariables = Exact<{ [key: string]: never; }>; -export type GetPublicWorkspaceDataBySubdomainQuery = { __typename?: 'Query', getPublicWorkspaceDataBySubdomain: { __typename?: 'PublicWorkspaceDataOutput', id: string, logo?: string | null, displayName?: string | null, subdomain: string, hostname?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, magicLink: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> } } }; +export type GetPublicWorkspaceDataByDomainQuery = { __typename?: 'Query', getPublicWorkspaceDataByDomain: { __typename?: 'PublicWorkspaceDataOutput', id: string, logo?: string | null, displayName?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, authProviders: { __typename?: 'AuthProviders', google: boolean, magicLink: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> } } }; export type ValidatePasswordResetTokenQueryVariables = Exact<{ token: Scalars['String']; @@ -2291,7 +2289,7 @@ export type ListSsoIdentityProvidersByWorkspaceIdQueryVariables = Exact<{ [key: export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdentityProviderType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, hostname?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string } | null }> }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, hostname?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, hostname?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -2308,7 +2306,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, hostname?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string } | null }> } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, hostname?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, hostname?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } }; export type ActivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -2399,7 +2397,7 @@ export type ActivateWorkspaceMutationVariables = Exact<{ }>; -export type ActivateWorkspaceMutation = { __typename?: 'Mutation', activateWorkspace: { __typename?: 'Workspace', id: any, subdomain: string } }; +export type ActivateWorkspaceMutation = { __typename?: 'Mutation', activateWorkspace: { __typename?: 'Workspace', id: any } }; export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>; @@ -2430,7 +2428,7 @@ export type GetWorkspaceFromInviteHashQueryVariables = Exact<{ }>; -export type GetWorkspaceFromInviteHashQuery = { __typename?: 'Query', findWorkspaceFromInviteHash: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, allowImpersonation: boolean, subdomain: string } }; +export type GetWorkspaceFromInviteHashQuery = { __typename?: 'Query', findWorkspaceFromInviteHash: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, allowImpersonation: boolean } }; export const TimelineCalendarEventParticipantFragmentFragmentDoc = gql` fragment TimelineCalendarEventParticipantFragment on TimelineCalendarEventParticipant { @@ -2583,6 +2581,10 @@ export const UserQueryFragmentFragmentDoc = gql` subdomain hasValidEnterpriseKey hostname + workspaceUrls { + subdomainUrl + customUrl + } featureFlags { id key @@ -2607,6 +2609,11 @@ export const UserQueryFragmentFragmentDoc = gql` logo displayName subdomain + hostname + workspaceUrls { + subdomainUrl + customUrl + } } } userVars @@ -3168,7 +3175,10 @@ export const ImpersonateDocument = gql` mutation Impersonate($userId: String!, $workspaceId: String!) { impersonate(userId: $userId, workspaceId: $workspaceId) { workspace { - subdomain + workspaceUrls { + subdomainUrl + customUrl + } id } loginToken { @@ -3287,7 +3297,10 @@ export const SignUpDocument = gql` } workspace { id - subdomain + workspaceUrls { + subdomainUrl + customUrl + } } } } @@ -3369,8 +3382,10 @@ export const CheckUserExistsDocument = gql` availableWorkspaces { id displayName - subdomain - hostname + workspaceUrls { + subdomainUrl + customUrl + } logo sso { type @@ -3417,14 +3432,16 @@ export function useCheckUserExistsLazyQuery(baseOptions?: Apollo.LazyQueryHookOp export type CheckUserExistsQueryHookResult = ReturnType; export type CheckUserExistsLazyQueryHookResult = ReturnType; export type CheckUserExistsQueryResult = Apollo.QueryResult; -export const GetPublicWorkspaceDataBySubdomainDocument = gql` - query GetPublicWorkspaceDataBySubdomain { - getPublicWorkspaceDataBySubdomain { +export const GetPublicWorkspaceDataByDomainDocument = gql` + query GetPublicWorkspaceDataByDomain { + getPublicWorkspaceDataByDomain { id logo displayName - subdomain - hostname + workspaceUrls { + subdomainUrl + customUrl + } authProviders { sso { id @@ -3443,31 +3460,31 @@ export const GetPublicWorkspaceDataBySubdomainDocument = gql` `; /** - * __useGetPublicWorkspaceDataBySubdomainQuery__ + * __useGetPublicWorkspaceDataByDomainQuery__ * - * To run a query within a React component, call `useGetPublicWorkspaceDataBySubdomainQuery` and pass it any options that fit your needs. - * When your component renders, `useGetPublicWorkspaceDataBySubdomainQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `useGetPublicWorkspaceDataByDomainQuery` and pass it any options that fit your needs. + * When your component renders, `useGetPublicWorkspaceDataByDomainQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example - * const { data, loading, error } = useGetPublicWorkspaceDataBySubdomainQuery({ + * const { data, loading, error } = useGetPublicWorkspaceDataByDomainQuery({ * variables: { * }, * }); */ -export function useGetPublicWorkspaceDataBySubdomainQuery(baseOptions?: Apollo.QueryHookOptions) { +export function useGetPublicWorkspaceDataByDomainQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(GetPublicWorkspaceDataBySubdomainDocument, options); + return Apollo.useQuery(GetPublicWorkspaceDataByDomainDocument, options); } -export function useGetPublicWorkspaceDataBySubdomainLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { +export function useGetPublicWorkspaceDataByDomainLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(GetPublicWorkspaceDataBySubdomainDocument, options); + return Apollo.useLazyQuery(GetPublicWorkspaceDataByDomainDocument, options); } -export type GetPublicWorkspaceDataBySubdomainQueryHookResult = ReturnType; -export type GetPublicWorkspaceDataBySubdomainLazyQueryHookResult = ReturnType; -export type GetPublicWorkspaceDataBySubdomainQueryResult = Apollo.QueryResult; +export type GetPublicWorkspaceDataByDomainQueryHookResult = ReturnType; +export type GetPublicWorkspaceDataByDomainLazyQueryHookResult = ReturnType; +export type GetPublicWorkspaceDataByDomainQueryResult = Apollo.QueryResult; export const ValidatePasswordResetTokenDocument = gql` query ValidatePasswordResetToken($token: String!) { validatePasswordResetToken(passwordResetToken: $token) { @@ -4698,7 +4715,6 @@ export const ActivateWorkspaceDocument = gql` mutation ActivateWorkspace($input: ActivateWorkspaceInput!) { activateWorkspace(data: $input) { id - subdomain } } `; @@ -4887,7 +4903,6 @@ export const GetWorkspaceFromInviteHashDocument = gql` displayName logo allowImpersonation - subdomain } } `; diff --git a/packages/twenty-front/src/loading/components/__stories__/UserOrMetadataLoader.stories.tsx b/packages/twenty-front/src/loading/components/__stories__/UserOrMetadataLoader.stories.tsx index 54ed635eb..1ea2d67f4 100644 --- a/packages/twenty-front/src/loading/components/__stories__/UserOrMetadataLoader.stories.tsx +++ b/packages/twenty-front/src/loading/components/__stories__/UserOrMetadataLoader.stories.tsx @@ -4,7 +4,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { within } from '@storybook/test'; import { HttpResponse, graphql } from 'msw'; -import { GET_PUBLIC_WORKSPACE_DATA_BY_SUBDOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataBySubdomain'; +import { GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataByDomain'; import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig'; import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; @@ -36,7 +36,7 @@ const userMetadataLoaderMocks = { }); }), graphql.query( - getOperationName(GET_PUBLIC_WORKSPACE_DATA_BY_SUBDOMAIN) ?? '', + getOperationName(GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN) ?? '', () => { return HttpResponse.json({ data: { diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/impersonate.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/impersonate.ts index 77e062cfa..e57027dc7 100644 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/impersonate.ts +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/impersonate.ts @@ -5,7 +5,10 @@ export const IMPERSONATE = gql` mutation Impersonate($userId: String!, $workspaceId: String!) { impersonate(userId: $userId, workspaceId: $workspaceId) { workspace { - subdomain + workspaceUrls { + subdomainUrl + customUrl + } id } loginToken { diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts index d15416e2e..d8c59e1ae 100644 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts @@ -22,7 +22,10 @@ export const SIGN_UP = gql` } workspace { id - subdomain + workspaceUrls { + subdomainUrl + customUrl + } } } } diff --git a/packages/twenty-front/src/modules/auth/graphql/queries/checkUserExists.ts b/packages/twenty-front/src/modules/auth/graphql/queries/checkUserExists.ts index 389a67061..ba635b2cc 100644 --- a/packages/twenty-front/src/modules/auth/graphql/queries/checkUserExists.ts +++ b/packages/twenty-front/src/modules/auth/graphql/queries/checkUserExists.ts @@ -9,8 +9,10 @@ export const CHECK_USER_EXISTS = gql` availableWorkspaces { id displayName - subdomain - hostname + workspaceUrls { + subdomainUrl + customUrl + } logo sso { type diff --git a/packages/twenty-front/src/modules/auth/graphql/queries/getPublicWorkspaceDataBySubdomain.ts b/packages/twenty-front/src/modules/auth/graphql/queries/getPublicWorkspaceDataByDomain.ts similarity index 58% rename from packages/twenty-front/src/modules/auth/graphql/queries/getPublicWorkspaceDataBySubdomain.ts rename to packages/twenty-front/src/modules/auth/graphql/queries/getPublicWorkspaceDataByDomain.ts index 4b5ad7efd..986c4f59e 100644 --- a/packages/twenty-front/src/modules/auth/graphql/queries/getPublicWorkspaceDataBySubdomain.ts +++ b/packages/twenty-front/src/modules/auth/graphql/queries/getPublicWorkspaceDataByDomain.ts @@ -1,13 +1,15 @@ import { gql } from '@apollo/client'; -export const GET_PUBLIC_WORKSPACE_DATA_BY_SUBDOMAIN = gql` - query GetPublicWorkspaceDataBySubdomain { - getPublicWorkspaceDataBySubdomain { +export const GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN = gql` + query GetPublicWorkspaceDataByDomain { + getPublicWorkspaceDataByDomain { id logo displayName - subdomain - hostname + workspaceUrls { + subdomainUrl + customUrl + } authProviders { sso { id diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index ca6bb20a1..506c28adf 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -53,7 +53,7 @@ import { BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type import { captchaState } from '@/client-config/states/captchaState'; import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState'; import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; -import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain'; +import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain'; import { useRedirect } from '@/domain-manager/hooks/useRedirect'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; @@ -62,6 +62,7 @@ import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/state import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState'; import { useSearchParams } from 'react-router-dom'; import { dynamicActivate } from '~/utils/i18n/dynamicActivate'; +import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; export const useAuth = () => { const setTokenPair = useSetRecoilState(tokenPairState); @@ -96,8 +97,7 @@ export const useAuth = () => { useGetLoginTokenFromEmailVerificationTokenMutation(); const [getCurrentUser] = useGetCurrentUserLazyQuery(); - const { isOnAWorkspaceSubdomain } = - useIsCurrentLocationOnAWorkspaceSubdomain(); + const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace(); const workspacePublicData = useRecoilValue(workspacePublicDataState); @@ -289,10 +289,10 @@ export const useAuth = () => { setCurrentWorkspace(workspace); - if (isDefined(workspace) && isOnAWorkspaceSubdomain) { + if (isDefined(workspace) && isOnAWorkspace) { setLastAuthenticateWorkspaceDomain({ workspaceId: workspace.id, - subdomain: workspace.subdomain, + workspaceUrl: getWorkspaceUrl(workspace.workspaceUrls), }); } @@ -315,7 +315,7 @@ export const useAuth = () => { }; }, [ getCurrentUser, - isOnAWorkspaceSubdomain, + isOnAWorkspace, setCurrentUser, setCurrentWorkspace, setCurrentWorkspaceMember, @@ -413,7 +413,8 @@ export const useAuth = () => { if (isMultiWorkspaceEnabled) { return redirectToWorkspaceDomain( - signUpResult.data.signUp.workspace.subdomain, + getWorkspaceUrl(signUpResult.data.signUp.workspace.workspaceUrls), + isEmailVerificationRequired ? AppPath.SignInUp : AppPath.Verify, { ...(!isEmailVerificationRequired && { diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx index ee7bf7ef0..dacb9ca44 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpGlobalScopeForm.tsx @@ -27,6 +27,7 @@ import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirect import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { isDefined } from 'twenty-shared'; +import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; const StyledContentContainer = styled(motion.div)` margin-bottom: ${({ theme }) => theme.spacing(8)}; @@ -92,9 +93,13 @@ export const SignInUpGlobalScopeForm = () => { if (response.__typename === 'UserExists') { if (response.availableWorkspaces.length >= 1) { const workspace = response.availableWorkspaces[0]; - return redirectToWorkspaceDomain(workspace.subdomain, pathname, { - email: form.getValues('email'), - }); + return redirectToWorkspaceDomain( + getWorkspaceUrl(workspace.workspaceUrls), + pathname, + { + email: form.getValues('email'), + }, + ); } } if (response.__typename === 'UserNotExists') { diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSSO.test.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSSO.test.tsx index 3b0189bc1..c01bcf78c 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSSO.test.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/__tests__/useSSO.test.tsx @@ -3,6 +3,7 @@ import { useSSO } from '@/auth/sign-in-up/hooks/useSSO'; import { useRedirect } from '@/domain-manager/hooks/useRedirect'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { MockedProvider } from '@apollo/client/testing'; +import { MemoryRouter } from 'react-router-dom'; import { renderHook } from '@testing-library/react'; jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar'); @@ -52,9 +53,11 @@ const apolloMocks = [ ]; const Wrapper = ({ children }: { children: React.ReactNode }) => ( - - {children} - + + + {children} + + ); describe('useSSO', () => { diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts index abe014412..412570233 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSSO.ts @@ -13,14 +13,16 @@ export const useSSO = () => { const { enqueueSnackBar } = useSnackBar(); const { redirect } = useRedirect(); - const redirectToSSOLoginPage = async (identityProviderId: string) => { let authorizationUrlForSSOResult; try { authorizationUrlForSSOResult = await apolloClient.mutate({ mutation: GET_AUTHORIZATION_URL, variables: { - input: { identityProviderId, workspaceInviteHash }, + input: { + identityProviderId, + workspaceInviteHash, + }, }, }); } catch (error: any) { diff --git a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts index d299d2246..87bdc0d7a 100644 --- a/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts +++ b/packages/twenty-front/src/modules/auth/states/currentWorkspaceState.ts @@ -21,6 +21,7 @@ export type CurrentWorkspace = Pick< | 'hasValidEnterpriseKey' | 'subdomain' | 'hostname' + | 'workspaceUrls' | 'metadataVersion' >; diff --git a/packages/twenty-front/src/modules/auth/states/workspaces.ts b/packages/twenty-front/src/modules/auth/states/workspaces.ts index b7e7c8758..de70cd7b2 100644 --- a/packages/twenty-front/src/modules/auth/states/workspaces.ts +++ b/packages/twenty-front/src/modules/auth/states/workspaces.ts @@ -1,10 +1,9 @@ import { createState } from '@ui/utilities/state/utils/createState'; - import { Workspace } from '~/generated/graphql'; export type Workspaces = Pick< Workspace, - 'id' | 'logo' | 'displayName' | 'subdomain' + 'id' | 'logo' | 'displayName' | 'workspaceUrls' >[]; export const workspacesState = createState({ diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useBuildWorkspaceUrl.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useBuildWorkspaceUrl.ts index 49d82b311..a3c9d9505 100644 --- a/packages/twenty-front/src/modules/domain-manager/hooks/useBuildWorkspaceUrl.ts +++ b/packages/twenty-front/src/modules/domain-manager/hooks/useBuildWorkspaceUrl.ts @@ -1,20 +1,12 @@ -import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; -import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared'; export const useBuildWorkspaceUrl = () => { - const domainConfiguration = useRecoilValue(domainConfigurationState); - const buildWorkspaceUrl = ( - subdomain: string, + endpoint: string, pathname?: string, - searchParams?: Record, + searchParams?: Record, ) => { - const url = new URL(window.location.href); - - if (subdomain.length !== 0) { - url.hostname = `${subdomain}.${domainConfiguration.frontDomain}`; - } + const url = new URL(endpoint); if (isDefined(pathname)) { url.pathname = pathname; @@ -22,7 +14,7 @@ export const useBuildWorkspaceUrl = () => { if (isDefined(searchParams)) { Object.entries(searchParams).forEach(([key, value]) => - url.searchParams.set(key, value), + url.searchParams.set(key, value.toString()), ); } return url.toString(); diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataByDomain.ts similarity index 77% rename from packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain.ts rename to packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataByDomain.ts index 047cd6469..abb95330d 100644 --- a/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain.ts +++ b/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataByDomain.ts @@ -5,9 +5,9 @@ import { useRedirectToDefaultDomain } from '@/domain-manager/hooks/useRedirectTo import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared'; -import { useGetPublicWorkspaceDataBySubdomainQuery } from '~/generated/graphql'; +import { useGetPublicWorkspaceDataByDomainQuery } from '~/generated/graphql'; -export const useGetPublicWorkspaceDataBySubdomain = () => { +export const useGetPublicWorkspaceDataByDomain = () => { const { isDefaultDomain } = useIsCurrentLocationOnDefaultDomain(); const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState); const setWorkspaceAuthProviders = useSetRecoilState( @@ -19,15 +19,15 @@ export const useGetPublicWorkspaceDataBySubdomain = () => { workspacePublicDataState, ); - const { loading, data, error } = useGetPublicWorkspaceDataBySubdomainQuery({ + const { loading, data, error } = useGetPublicWorkspaceDataByDomainQuery({ skip: (isMultiWorkspaceEnabled && isDefaultDomain) || isDefined(workspacePublicData), onCompleted: (data) => { setWorkspaceAuthProviders( - data.getPublicWorkspaceDataBySubdomain.authProviders, + data.getPublicWorkspaceDataByDomain.authProviders, ); - setWorkspacePublicDataState(data.getPublicWorkspaceDataBySubdomain); + setWorkspacePublicDataState(data.getPublicWorkspaceDataByDomain); }, onError: (error) => { // eslint-disable-next-line no-console @@ -38,7 +38,7 @@ export const useGetPublicWorkspaceDataBySubdomain = () => { return { loading, - data: data?.getPublicWorkspaceDataBySubdomain, + data: data?.getPublicWorkspaceDataByDomain, error, }; }; diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useIsCurrentLocationOnAWorkspace.ts similarity index 88% rename from packages/twenty-front/src/modules/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain.ts rename to packages/twenty-front/src/modules/domain-manager/hooks/useIsCurrentLocationOnAWorkspace.ts index 4920f9f63..402da7361 100644 --- a/packages/twenty-front/src/modules/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain.ts +++ b/packages/twenty-front/src/modules/domain-manager/hooks/useIsCurrentLocationOnAWorkspace.ts @@ -4,7 +4,7 @@ import { domainConfigurationState } from '@/domain-manager/states/domainConfigur import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared'; -export const useIsCurrentLocationOnAWorkspaceSubdomain = () => { +export const useIsCurrentLocationOnAWorkspace = () => { const { defaultDomain } = useReadDefaultDomainFromConfiguration(); const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState); @@ -18,10 +18,10 @@ export const useIsCurrentLocationOnAWorkspaceSubdomain = () => { throw new Error('frontDomain and defaultSubdomain are required'); } - const isOnAWorkspaceSubdomain = + const isOnAWorkspace = isMultiWorkspaceEnabled && window.location.hostname !== defaultDomain; return { - isOnAWorkspaceSubdomain, + isOnAWorkspace, }; }; diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain.ts index 99348f3fb..f431176d9 100644 --- a/packages/twenty-front/src/modules/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain.ts +++ b/packages/twenty-front/src/modules/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain.ts @@ -8,7 +8,7 @@ export const useLastAuthenticatedWorkspaceDomain = () => { lastAuthenticatedWorkspaceDomainState, ); const setLastAuthenticateWorkspaceDomainWithCookieAttributes = ( - params: { workspaceId: string; subdomain: string } | null, + params: { workspaceId: string; workspaceUrl: string } | null, ) => { setLastAuthenticatedWorkspaceDomain({ ...(params ? params : {}), diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation.ts deleted file mode 100644 index 0fb57acf6..000000000 --- a/packages/twenty-front/src/modules/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain'; -import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; -import { useRecoilValue } from 'recoil'; -import { isDefined } from 'twenty-shared'; - -export const useReadWorkspaceSubdomainFromCurrentLocation = () => { - const domainConfiguration = useRecoilValue(domainConfigurationState); - const { isOnAWorkspaceSubdomain } = - useIsCurrentLocationOnAWorkspaceSubdomain(); - - if (!isDefined(domainConfiguration.frontDomain)) { - throw new Error('frontDomain is not defined'); - } - - const workspaceSubdomain = isOnAWorkspaceSubdomain - ? window.location.hostname.replace( - `.${domainConfiguration.frontDomain}`, - '', - ) - : null; - - return { - workspaceSubdomain, - }; -}; diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useReadWorkspaceUrlFromCurrentLocation.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useReadWorkspaceUrlFromCurrentLocation.ts new file mode 100644 index 000000000..e24ef66e4 --- /dev/null +++ b/packages/twenty-front/src/modules/domain-manager/hooks/useReadWorkspaceUrlFromCurrentLocation.ts @@ -0,0 +1,11 @@ +import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; + +export const useReadWorkspaceUrlFromCurrentLocation = () => { + const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace(); + + return { + currentLocationHostname: isOnAWorkspace + ? window.location.hostname + : undefined, + }; +}; diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToWorkspaceDomain.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToWorkspaceDomain.ts index 07f881156..a1317ac50 100644 --- a/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToWorkspaceDomain.ts +++ b/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToWorkspaceDomain.ts @@ -9,12 +9,12 @@ export const useRedirectToWorkspaceDomain = () => { const { redirect } = useRedirect(); const redirectToWorkspaceDomain = ( - subdomain: string, + baseUrl: string, pathname?: string, - searchParams?: Record, + searchParams?: Record, ) => { if (!isMultiWorkspaceEnabled) return; - redirect(buildWorkspaceUrl(subdomain, pathname, searchParams)); + redirect(buildWorkspaceUrl(baseUrl, pathname, searchParams)); }; return { diff --git a/packages/twenty-front/src/modules/domain-manager/states/lastAuthenticatedWorkspaceDomainState.ts b/packages/twenty-front/src/modules/domain-manager/states/lastAuthenticatedWorkspaceDomainState.ts index 67c44f43f..040f7fc93 100644 --- a/packages/twenty-front/src/modules/domain-manager/states/lastAuthenticatedWorkspaceDomainState.ts +++ b/packages/twenty-front/src/modules/domain-manager/states/lastAuthenticatedWorkspaceDomainState.ts @@ -3,7 +3,7 @@ import { cookieStorageEffect } from '~/utils/recoil-effects'; export const lastAuthenticatedWorkspaceDomainState = createState< | { - subdomain: string; + workspaceUrl: string; workspaceId: string; cookieAttributes?: Cookies.CookieAttributes; } diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts index d0a51a08f..83c153d57 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts @@ -159,6 +159,10 @@ export const queries = { subdomain hasValidEnterpriseKey hostname + workspaceUrls { + subdomainUrl + customUrl + } featureFlags { id key @@ -183,6 +187,11 @@ export const queries = { logo displayName subdomain + hostname + workspaceUrls { + subdomainUrl + customUrl + } } } userVars @@ -309,6 +318,10 @@ export const responseData = { isPasswordAuthEnabled: true, subdomain: 'test', hostname: null, + workspaceUrls: { + customUrl: undefined, + subdomainUrl: 'https://test.twenty.com/', + }, featureFlags: [], metadataVersion: 1, currentBillingSubscription: null, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts index 42b12ed46..90e76e6d4 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts @@ -27,6 +27,10 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({ isGoogleAuthEnabled: true, isMicrosoftAuthEnabled: false, isPasswordAuthEnabled: true, + workspaceUrls: { + subdomainUrl: 'https://twenty.twenty.com', + customUrl: 'https://my-custom-domain.com', + }, currentBillingSubscription: { id: '1', interval: SubscriptionInterval.Month, diff --git a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts index e4ce97d95..67e26d02f 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts +++ b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts @@ -8,6 +8,7 @@ import { useState } from 'react'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared'; import { useImpersonateMutation } from '~/generated/graphql'; +import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; export const useImpersonate = () => { const [currentUser] = useRecoilState(currentUserState); @@ -55,9 +56,13 @@ export const useImpersonate = () => { return; } - return redirectToWorkspaceDomain(workspace.subdomain, AppPath.Verify, { - loginToken: loginToken.token, - }); + return redirectToWorkspaceDomain( + getWorkspaceUrl(workspace.workspaceUrls), + AppPath.Verify, + { + loginToken: loginToken.token, + }, + ); } catch (error) { setError('Failed to impersonate user. Please try again.'); setIsLoading(false); diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx index f02f6341a..87b443626 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx @@ -18,6 +18,7 @@ import { UndecoratedLink, } from 'twenty-ui'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; +import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; const StyledContainer = styled.div<{ isNavigationDrawerExpanded: boolean }>` align-items: center; @@ -67,7 +68,7 @@ export const MultiWorkspaceDropdownButton = ({ const { buildWorkspaceUrl } = useBuildWorkspaceUrl(); const handleChange = async (workspace: Workspaces[0]) => { - redirectToWorkspaceDomain(workspace.subdomain); + redirectToWorkspaceDomain(getWorkspaceUrl(workspace.workspaceUrls)); }; const [isNavigationDrawerExpanded] = useRecoilState( isNavigationDrawerExpandedState, @@ -104,7 +105,7 @@ export const MultiWorkspaceDropdownButton = ({ {workspaces.map((workspace) => ( { event?.preventDefault(); handleChange(workspace); diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index 607afc377..54666b0a1 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -38,6 +38,10 @@ export const USER_QUERY_FRAGMENT = gql` subdomain hasValidEnterpriseKey hostname + workspaceUrls { + subdomainUrl + customUrl + } featureFlags { id key @@ -62,6 +66,11 @@ export const USER_QUERY_FRAGMENT = gql` logo displayName subdomain + hostname + workspaceUrls { + subdomainUrl + customUrl + } } } userVars diff --git a/packages/twenty-front/src/modules/workspace/components/WorkspaceProviderEffect.tsx b/packages/twenty-front/src/modules/workspace/components/WorkspaceProviderEffect.tsx index 6630050a5..63b5a0f3e 100644 --- a/packages/twenty-front/src/modules/workspace/components/WorkspaceProviderEffect.tsx +++ b/packages/twenty-front/src/modules/workspace/components/WorkspaceProviderEffect.tsx @@ -1,17 +1,18 @@ import { useRecoilValue } from 'recoil'; -import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; -import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation'; -import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; -import { lastAuthenticatedWorkspaceDomainState } from '@/domain-manager/states/lastAuthenticatedWorkspaceDomainState'; import { useEffect } from 'react'; import { isDefined } from 'twenty-shared'; +import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; +import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; +import { lastAuthenticatedWorkspaceDomainState } from '@/domain-manager/states/lastAuthenticatedWorkspaceDomainState'; +import { useReadWorkspaceUrlFromCurrentLocation } from '@/domain-manager/hooks/useReadWorkspaceUrlFromCurrentLocation'; -import { useGetPublicWorkspaceDataBySubdomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain'; import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain'; +import { useGetPublicWorkspaceDataByDomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataByDomain'; +import { WorkspaceUrls } from '~/generated/graphql'; +import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; export const WorkspaceProviderEffect = () => { - const { data: getPublicWorkspaceData } = - useGetPublicWorkspaceDataBySubdomain(); + const { data: getPublicWorkspaceData } = useGetPublicWorkspaceDataByDomain(); const lastAuthenticatedWorkspaceDomain = useRecoilValue( lastAuthenticatedWorkspaceDomainState, @@ -20,23 +21,38 @@ export const WorkspaceProviderEffect = () => { const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); const { isDefaultDomain } = useIsCurrentLocationOnDefaultDomain(); - const { workspaceSubdomain } = useReadWorkspaceSubdomainFromCurrentLocation(); + const { currentLocationHostname } = useReadWorkspaceUrlFromCurrentLocation(); const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState); + const getHostnamesFromWorkspaceUrls = (workspaceUrls: WorkspaceUrls) => { + return { + customUrlHostname: workspaceUrls.customUrl + ? new URL(workspaceUrls.customUrl).hostname + : undefined, + subdomainUrlHostname: new URL(workspaceUrls.subdomainUrl).hostname, + }; + }; + useEffect(() => { + const hostnames = getPublicWorkspaceData + ? getHostnamesFromWorkspaceUrls(getPublicWorkspaceData?.workspaceUrls) + : null; if ( isMultiWorkspaceEnabled && - isDefined(getPublicWorkspaceData?.subdomain) && - getPublicWorkspaceData.subdomain !== workspaceSubdomain + isDefined(getPublicWorkspaceData) && + currentLocationHostname !== hostnames?.customUrlHostname && + currentLocationHostname !== hostnames?.subdomainUrlHostname ) { - redirectToWorkspaceDomain(getPublicWorkspaceData.subdomain); + redirectToWorkspaceDomain( + getWorkspaceUrl(getPublicWorkspaceData.workspaceUrls), + ); } }, [ - workspaceSubdomain, isMultiWorkspaceEnabled, redirectToWorkspaceDomain, getPublicWorkspaceData, + currentLocationHostname, ]); useEffect(() => { @@ -44,10 +60,10 @@ export const WorkspaceProviderEffect = () => { isMultiWorkspaceEnabled && isDefaultDomain && isDefined(lastAuthenticatedWorkspaceDomain) && - 'subdomain' in lastAuthenticatedWorkspaceDomain && - isDefined(lastAuthenticatedWorkspaceDomain?.subdomain) + 'workspaceUrl' in lastAuthenticatedWorkspaceDomain && + isDefined(lastAuthenticatedWorkspaceDomain?.workspaceUrl) ) { - redirectToWorkspaceDomain(lastAuthenticatedWorkspaceDomain.subdomain); + redirectToWorkspaceDomain(lastAuthenticatedWorkspaceDomain.workspaceUrl); } }, [ isMultiWorkspaceEnabled, diff --git a/packages/twenty-front/src/modules/workspace/graphql/mutations/activateWorkspace.ts b/packages/twenty-front/src/modules/workspace/graphql/mutations/activateWorkspace.ts index 73e6d80b1..b116a298a 100644 --- a/packages/twenty-front/src/modules/workspace/graphql/mutations/activateWorkspace.ts +++ b/packages/twenty-front/src/modules/workspace/graphql/mutations/activateWorkspace.ts @@ -4,7 +4,6 @@ export const ACTIVATE_WORKSPACE = gql` mutation ActivateWorkspace($input: ActivateWorkspaceInput!) { activateWorkspace(data: $input) { id - subdomain } } `; diff --git a/packages/twenty-front/src/modules/workspace/graphql/queries/getWorkspaceFromInviteHash.ts b/packages/twenty-front/src/modules/workspace/graphql/queries/getWorkspaceFromInviteHash.ts index 5b7925f63..b18a9d2c6 100644 --- a/packages/twenty-front/src/modules/workspace/graphql/queries/getWorkspaceFromInviteHash.ts +++ b/packages/twenty-front/src/modules/workspace/graphql/queries/getWorkspaceFromInviteHash.ts @@ -7,7 +7,6 @@ export const GET_WORKSPACE_FROM_INVITE_HASH = gql` displayName logo allowImpersonation - subdomain } } `; diff --git a/packages/twenty-front/src/pages/auth/SignInUp.tsx b/packages/twenty-front/src/pages/auth/SignInUp.tsx index cbb850b13..d45d30ced 100644 --- a/packages/twenty-front/src/pages/auth/SignInUp.tsx +++ b/packages/twenty-front/src/pages/auth/SignInUp.tsx @@ -13,8 +13,8 @@ import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/componen import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm'; import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeFormEffect'; import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; -import { useGetPublicWorkspaceDataBySubdomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain'; -import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain'; +import { useGetPublicWorkspaceDataByDomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataByDomain'; +import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain'; import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName'; import { useMemo } from 'react'; @@ -24,7 +24,7 @@ import { AnimatedEaseIn } from 'twenty-ui'; import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash'; import { useLingui } from '@lingui/react/macro'; import { useSearchParams } from 'react-router-dom'; -import { PublicWorkspaceDataOutput } from '~/generated-metadata/graphql'; +import { PublicWorkspaceDataOutput } from '~/generated/graphql'; const StandardContent = ({ workspacePublicData, @@ -55,10 +55,9 @@ export const SignInUp = () => { const { form } = useSignInUpForm(); const { signInUpStep } = useSignInUp(form); const { isDefaultDomain } = useIsCurrentLocationOnDefaultDomain(); - const { isOnAWorkspaceSubdomain } = - useIsCurrentLocationOnAWorkspaceSubdomain(); + const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace(); const workspacePublicData = useRecoilValue(workspacePublicDataState); - const { loading } = useGetPublicWorkspaceDataBySubdomain(); + const { loading } = useGetPublicWorkspaceDataByDomain(); const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState); const { workspaceInviteHash, workspace: workspaceFromInviteHash } = useWorkspaceFromInviteHash(); @@ -91,7 +90,7 @@ export const SignInUp = () => { if ( (!isMultiWorkspaceEnabled || - (isMultiWorkspaceEnabled && isOnAWorkspaceSubdomain)) && + (isMultiWorkspaceEnabled && isOnAWorkspace)) && signInUpStep === SignInUpStep.SSOIdentityProviderSelection ) { return ; @@ -99,7 +98,7 @@ export const SignInUp = () => { if ( isDefined(workspacePublicData) && - (!isMultiWorkspaceEnabled || isOnAWorkspaceSubdomain) + (!isMultiWorkspaceEnabled || isOnAWorkspace) ) { return ( <> @@ -113,7 +112,7 @@ export const SignInUp = () => { }, [ isDefaultDomain, isMultiWorkspaceEnabled, - isOnAWorkspaceSubdomain, + isOnAWorkspace, loading, signInUpStep, workspacePublicData, diff --git a/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx b/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx index f474a581e..f702e2b37 100644 --- a/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx +++ b/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx @@ -1,3 +1,4 @@ +import { ApolloError } from '@apollo/client'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useRecoilState } from 'recoil'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; @@ -13,7 +14,6 @@ import { SettingsSubdomain } from '~/pages/settings/workspace/SettingsSubdomain' import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; -import { ApolloError } from '@apollo/client'; import { Trans, useLingui } from '@lingui/react/macro'; import { z } from 'zod'; import { FormProvider, useForm } from 'react-hook-form'; @@ -94,12 +94,18 @@ export const SettingsDomain = () => { }); }, onCompleted: () => { + const currentUrl = new URL(window.location.href); + + currentUrl.hostname = new URL( + currentWorkspace.workspaceUrls.subdomainUrl, + ).hostname.replace(currentWorkspace.subdomain, values.subdomain); + setCurrentWorkspace({ ...currentWorkspace, subdomain: values.subdomain, }); - redirectToWorkspaceDomain(values.subdomain); + redirectToWorkspaceDomain(currentUrl.toString()); }, }); }; diff --git a/packages/twenty-front/src/pages/settings/workspace/SettingsHostname.tsx b/packages/twenty-front/src/pages/settings/workspace/SettingsHostname.tsx index 300ffad6e..d01d412db 100644 --- a/packages/twenty-front/src/pages/settings/workspace/SettingsHostname.tsx +++ b/packages/twenty-front/src/pages/settings/workspace/SettingsHostname.tsx @@ -1,5 +1,4 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import styled from '@emotion/styled'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -41,7 +40,6 @@ export const SettingsHostname = () => { const [updateWorkspace] = useUpdateWorkspaceMutation(); const { data: getHostnameDetailsData } = useGetHostnameDetailsQuery(); - const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); const { t } = useLingui(); const [currentWorkspace, setCurrentWorkspace] = useRecoilState( @@ -75,8 +73,6 @@ export const SettingsHostname = () => { }, }, }); - - redirectToWorkspaceDomain(currentWorkspace.subdomain); } catch (error) { control.setError('hostname', { type: 'manual', @@ -106,8 +102,6 @@ export const SettingsHostname = () => { ...currentWorkspace, hostname: values.hostname, }); - - // redirectToWorkspaceDomain(values.subdomain); } catch (error) { control.setError('hostname', { type: 'manual', @@ -139,12 +133,36 @@ export const SettingsHostname = () => { {isDefined(getHostnameDetailsData?.getHostnameDetails?.hostname) && (
           {getHostnameDetailsData.getHostnameDetails.hostname} CNAME
-          app.twenty-main.com
+          twenty-main.com
         
)} - {getHostnameDetailsData && ( -
{JSON.stringify(getHostnameDetailsData, null, 4)}
- )} + {getHostnameDetailsData?.getHostnameDetails && + getHostnameDetailsData.getHostnameDetails.ownershipVerifications.map( + (ownershipVerification) => { + if ( + ownershipVerification.__typename === + 'CustomHostnameOwnershipVerificationTxt' + ) { + return ( +
+                  {ownershipVerification.name} TXT {ownershipVerification.value}
+                
+ ); + } + + if ( + ownershipVerification.__typename === + 'CustomHostnameOwnershipVerificationHttp' + ) { + return ( +
+                  {ownershipVerification.url} HTTP {ownershipVerification.body}
+                
+ ); + } + return <>; + }, + )} ); }; diff --git a/packages/twenty-front/src/testing/graphqlMocks.ts b/packages/twenty-front/src/testing/graphqlMocks.ts index 87575f919..f94bc8467 100644 --- a/packages/twenty-front/src/testing/graphqlMocks.ts +++ b/packages/twenty-front/src/testing/graphqlMocks.ts @@ -20,7 +20,7 @@ import { mockedUserData } from '~/testing/mock-data/users'; import { mockedViewsData } from '~/testing/mock-data/views'; import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members'; -import { GET_PUBLIC_WORKSPACE_DATA_BY_SUBDOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataBySubdomain'; +import { GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataByDomain'; import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/mock-metadata-query-result'; import { mockedTasks } from '~/testing/mock-data/tasks'; import { @@ -49,15 +49,18 @@ export const graphqlMocks = { }); }), graphql.query( - getOperationName(GET_PUBLIC_WORKSPACE_DATA_BY_SUBDOMAIN) ?? '', + getOperationName(GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN) ?? '', () => { return HttpResponse.json({ data: { - getPublicWorkspaceDataBySubdomain: { + getPublicWorkspaceDataByDomain: { id: 'id', logo: 'logo', displayName: 'displayName', - subdomain: 'subdomain', + workspaceUrls: { + customUrl: undefined, + subdomainUrl: 'https://twenty.com', + }, authProviders: { google: true, microsoft: false, diff --git a/packages/twenty-front/src/testing/mock-data/publicWorkspaceDataBySubdomain.ts b/packages/twenty-front/src/testing/mock-data/publicWorkspaceDataBySubdomain.ts index 96bdb729c..1ead7242e 100644 --- a/packages/twenty-front/src/testing/mock-data/publicWorkspaceDataBySubdomain.ts +++ b/packages/twenty-front/src/testing/mock-data/publicWorkspaceDataBySubdomain.ts @@ -1,12 +1,15 @@ -import { GetPublicWorkspaceDataBySubdomainQuery } from '~/generated/graphql'; +import { GetPublicWorkspaceDataByDomainQuery } from '~/generated/graphql'; -export const mockedPublicWorkspaceDataBySubdomain: GetPublicWorkspaceDataBySubdomainQuery['getPublicWorkspaceDataBySubdomain'] = +export const mockedPublicWorkspaceDataBySubdomain: GetPublicWorkspaceDataByDomainQuery['getPublicWorkspaceDataByDomain'] = { __typename: 'PublicWorkspaceDataOutput', id: '9870323e-22c3-4d14-9b7f-5bdc84f7d6ee', logo: 'workspace-logo/original/c88deb49-7636-4560-918d-08c3265ffb20.49?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3b3Jrc3BhY2VJZCI6Ijk4NzAzMjNlLTIyYzMtNGQxNC05YjdmLTViZGM4NGY3ZDZlZSIsImlhdCI6MTczNjU0MDU0MywiZXhwIjoxNzM2NjI2OTQzfQ.C8cnHu09VGseRbQAMM4nhiO6z4TLG03ntFTvxm53-xg', displayName: 'Twenty Eng', - subdomain: 'twenty-eng', + workspaceUrls: { + customUrl: 'https://twenty-eng.com', + subdomainUrl: 'https://custom.twenty.com', + }, authProviders: { __typename: 'AuthProviders', sso: [], diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index 8761be8ec..29154aa82 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -48,6 +48,10 @@ export const mockCurrentWorkspace: Workspace = { hasValidEnterpriseKey: false, isGoogleAuthEnabled: true, isPasswordAuthEnabled: true, + workspaceUrls: { + customUrl: undefined, + subdomainUrl: 'twenty.twenty.com', + }, isMicrosoftAuthEnabled: false, featureFlags: [ { diff --git a/packages/twenty-front/src/utils/getWorkspaceUrl.ts b/packages/twenty-front/src/utils/getWorkspaceUrl.ts new file mode 100644 index 000000000..d55a7c729 --- /dev/null +++ b/packages/twenty-front/src/utils/getWorkspaceUrl.ts @@ -0,0 +1,5 @@ +import { WorkspaceUrls } from '~/generated/graphql'; + +export const getWorkspaceUrl = (workspaceUrls: WorkspaceUrls) => { + return workspaceUrls.customUrl ?? workspaceUrls.subdomainUrl; +}; diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts index c36e0d574..8f5a1051d 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts @@ -48,7 +48,7 @@ export const seedFeatureFlags = async ( { key: FeatureFlagKey.IsCustomDomainEnabled, workspaceId: workspaceId, - value: true, + value: false, }, { key: FeatureFlagKey.IsBillingPlansEnabled, diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts index 095d8dd46..8ff0eff86 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts @@ -60,7 +60,7 @@ export class CoreQueryBuilderFactory { throw new BadRequestException( `No object was found for the workspace associated with this API key. You may generate a new one here ${this.domainManagerService .buildWorkspaceURL({ - subdomain: workspace.subdomain, + workspace, pathname: '/settings/developers', }) .toString()}`, diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.spec.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.spec.ts index 65d775279..e572cedac 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.spec.ts @@ -13,6 +13,7 @@ import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/featu import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; const UserFindOneMock = jest.fn(); const WorkspaceFindOneMock = jest.fn(); @@ -95,6 +96,15 @@ describe('AdminPanelService', () => { generateLoginToken: LoginTokenServiceGenerateLoginTokenMock, }, }, + { + provide: DomainManagerService, + useValue: { + getworkspaceUrls: jest.fn().mockReturnValue({ + customUrl: undefined, + subdomainUrl: 'https://twenty.twenty.com', + }), + }, + }, { provide: EnvironmentService, useValue: { @@ -230,7 +240,10 @@ describe('AdminPanelService', () => { expect.objectContaining({ workspace: { id: 'workspace-id', - subdomain: 'example-subdomain', + workspaceUrls: { + customUrl: undefined, + subdomainUrl: 'https://twenty.twenty.com', + }, }, loginToken: expect.objectContaining({ token: 'mock-login-token', diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts index f0e4222f8..5e929e10a 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts @@ -7,11 +7,13 @@ import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; @Module({ imports: [ TypeOrmModule.forFeature([User, Workspace, FeatureFlag], 'core'), AuthModule, + DomainManagerModule, ], providers: [AdminPanelResolver, AdminPanelService], exports: [AdminPanelService], diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts index 363958b72..21c5ffe9c 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts @@ -28,12 +28,14 @@ import { User } from 'src/engine/core-modules/user/user.entity'; import { userValidator } from 'src/engine/core-modules/user/user.validate'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; @Injectable() export class AdminPanelService { constructor( private readonly loginTokenService: LoginTokenService, private readonly environmentService: EnvironmentService, + private readonly domainManagerService: DomainManagerService, @InjectRepository(User, 'core') private readonly userRepository: Repository, @InjectRepository(Workspace, 'core') @@ -72,7 +74,9 @@ export class AdminPanelService { return { workspace: { id: user.workspaces[0].workspace.id, - subdomain: user.workspaces[0].workspace.subdomain, + workspaceUrls: this.domainManagerService.getworkspaceUrls( + user.workspaces[0].workspace, + ), }, loginToken, }; diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/impersonate.output.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/impersonate.output.ts index 7d7ccbd9b..d1065bc71 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/impersonate.output.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/impersonate.output.ts @@ -1,13 +1,13 @@ import { Field, ObjectType } from '@nestjs/graphql'; import { AuthToken } from 'src/engine/core-modules/auth/dto/token.entity'; -import { WorkspaceSubdomainAndId } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto'; +import { workspaceUrlsAndId } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto'; @ObjectType() export class ImpersonateOutput { @Field(() => AuthToken) loginToken: AuthToken; - @Field(() => WorkspaceSubdomainAndId) - workspace: WorkspaceSubdomainAndId; + @Field(() => workspaceUrlsAndId) + workspace: workspaceUrlsAndId; } diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index cf7db2ac6..fae9918da 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -221,7 +221,7 @@ export class AuthResolver { await this.emailVerificationService.sendVerificationEmail( user.id, user.email, - workspace.subdomain, + workspace, ); const loginToken = await this.loginTokenService.generateLoginToken( @@ -233,7 +233,7 @@ export class AuthResolver { loginToken, workspace: { id: workspace.id, - subdomain: workspace.subdomain, + workspaceUrls: this.domainManagerService.getworkspaceUrls(workspace), }, }; } diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts index 3264a91ea..65c3e0843 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts @@ -113,7 +113,7 @@ export class GoogleAPIsAuthController { return res.redirect( this.domainManagerService .buildWorkspaceURL({ - subdomain: workspace.subdomain, + workspace, pathname: redirectLocation || '/settings/accounts', }) .toString(), diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts index d7bf1d479..cc641d6f1 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts @@ -19,7 +19,6 @@ import { AuthService } from 'src/engine/core-modules/auth/services/auth.service' import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; import { User } from 'src/engine/core-modules/user/user.entity'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; @Controller('auth/google') @@ -28,7 +27,6 @@ export class GoogleAuthController { constructor( private readonly loginTokenService: LoginTokenService, private readonly authService: AuthService, - private readonly environmentService: EnvironmentService, private readonly guardRedirectService: GuardRedirectService, @InjectRepository(User, 'core') private readonly userRepository: Repository, @@ -110,7 +108,7 @@ export class GoogleAuthController { return res.redirect( this.authService.computeRedirectURI({ loginToken: loginToken.token, - subdomain: workspace.subdomain, + workspace, billingCheckoutSessionState, }), ); @@ -118,9 +116,9 @@ export class GoogleAuthController { return res.redirect( this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions( err, - currentWorkspace ?? { - subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), - }, + this.guardRedirectService.getSubdomainAndHostnameFromWorkspace( + currentWorkspace, + ), ), ); } diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts index af8eada63..8a415807f 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts @@ -120,7 +120,7 @@ export class MicrosoftAPIsAuthController { return res.redirect( this.domainManagerService .buildWorkspaceURL({ - subdomain: workspace.subdomain, + workspace, pathname: redirectLocation || '/settings/accounts', }) .toString(), diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts index 7b7b6a6f6..a2f19a252 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-auth.controller.ts @@ -18,7 +18,6 @@ import { AuthService } from 'src/engine/core-modules/auth/services/auth.service' import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; import { User } from 'src/engine/core-modules/user/user.entity'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; @Controller('auth/microsoft') @@ -28,7 +27,6 @@ export class MicrosoftAuthController { private readonly loginTokenService: LoginTokenService, private readonly authService: AuthService, private readonly guardRedirectService: GuardRedirectService, - private readonly environmentService: EnvironmentService, @InjectRepository(User, 'core') private readonly userRepository: Repository, ) {} @@ -111,8 +109,7 @@ export class MicrosoftAuthController { return res.redirect( this.authService.computeRedirectURI({ loginToken: loginToken.token, - subdomain: workspace.subdomain, - + workspace, billingCheckoutSessionState, }), ); @@ -120,9 +117,9 @@ export class MicrosoftAuthController { return res.redirect( this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions( err, - currentWorkspace ?? { - subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), - }, + this.guardRedirectService.getSubdomainAndHostnameFromWorkspace( + currentWorkspace, + ), ), ); } diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts index b791b21d5..b7f20410a 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts @@ -32,7 +32,6 @@ import { } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { SAMLRequest } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy'; import { OIDCRequest } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy'; @@ -45,7 +44,6 @@ export class SSOAuthController { private readonly loginTokenService: LoginTokenService, private readonly authService: AuthService, private readonly guardRedirectService: GuardRedirectService, - private readonly environmentService: EnvironmentService, private readonly sSOService: SSOService, @InjectRepository(User, 'core') private readonly userRepository: Repository, @@ -152,16 +150,16 @@ export class SSOAuthController { return res.redirect( this.authService.computeRedirectURI({ loginToken: loginToken.token, - subdomain: currentWorkspace.subdomain, + workspace: currentWorkspace, }), ); } catch (err) { return res.redirect( this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions( err, - workspaceIdentityProvider?.workspace ?? { - subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), - }, + this.guardRedirectService.getSubdomainAndHostnameFromWorkspace( + workspaceIdentityProvider?.workspace, + ), ), ); } diff --git a/packages/twenty-server/src/engine/core-modules/auth/dto/available-workspaces.output.ts b/packages/twenty-server/src/engine/core-modules/auth/dto/available-workspaces.output.ts index 45d01281b..f8e6d6a49 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/dto/available-workspaces.output.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/dto/available-workspaces.output.ts @@ -7,6 +7,7 @@ import { IdentityProviderType, SSOIdentityProviderStatus, } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; +import { workspaceUrls } from 'src/engine/core-modules/workspace/dtos/workspace-endpoints.dto'; @ObjectType() class SSOConnection { @@ -34,8 +35,8 @@ export class AvailableWorkspaceOutput { @Field(() => String, { nullable: true }) displayName?: string; - @Field(() => String) - subdomain: string; + @Field(() => workspaceUrls) + workspaceUrls: workspaceUrls; @Field(() => String, { nullable: true }) hostname?: string; diff --git a/packages/twenty-server/src/engine/core-modules/auth/dto/sign-up.output.ts b/packages/twenty-server/src/engine/core-modules/auth/dto/sign-up.output.ts index cdc44dbef..2e6367799 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/dto/sign-up.output.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/dto/sign-up.output.ts @@ -1,6 +1,6 @@ import { Field, ObjectType } from '@nestjs/graphql'; -import { WorkspaceSubdomainAndId } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto'; +import { workspaceUrlsAndId } from 'src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto'; import { AuthToken } from './token.entity'; @@ -9,6 +9,6 @@ export class SignUpOutput { @Field(() => AuthToken) loginToken: AuthToken; - @Field(() => WorkspaceSubdomainAndId) - workspace: WorkspaceSubdomainAndId; + @Field(() => workspaceUrlsAndId) + workspace: workspaceUrlsAndId; } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/enterprise-features-enabled.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/enterprise-features-enabled.guard.ts index 9a1eb5cb8..184b37922 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/enterprise-features-enabled.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/enterprise-features-enabled.guard.ts @@ -27,9 +27,11 @@ export class EnterpriseFeaturesEnabledGuard implements CanActivate { return true; } catch (err) { - this.guardRedirectService.dispatchErrorFromGuard(context, err, { - subdomain: this.guardRedirectService.getSubdomainFromContext(context), - }); + this.guardRedirectService.dispatchErrorFromGuard( + context, + err, + this.guardRedirectService.getSubdomainAndHostnameFromContext(context), + ); return false; } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts index 963c9a9a8..2c6749f61 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts @@ -50,9 +50,11 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard( return (await super.canActivate(context)) as boolean; } catch (err) { - this.guardRedirectService.dispatchErrorFromGuard(context, err, { - subdomain: this.guardRedirectService.getSubdomainFromContext(context), - }); + this.guardRedirectService.dispatchErrorFromGuard( + context, + err, + this.guardRedirectService.getSubdomainAndHostnameFromContext(context), + ); return false; } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts index 25e396736..55e692ee9 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts @@ -71,9 +71,9 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') { this.guardRedirectService.dispatchErrorFromGuard( context, err, - workspace ?? { - subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), - }, + this.guardRedirectService.getSubdomainAndHostnameFromWorkspace( + workspace, + ), ); return false; diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts index 96d9bfd33..8696cd6c1 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts @@ -11,13 +11,11 @@ import { } from 'src/engine/core-modules/auth/auth.exception'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; @Injectable() export class GoogleOauthGuard extends AuthGuard('google') { constructor( private readonly guardRedirectService: GuardRedirectService, - private readonly environmentService: EnvironmentService, @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, ) { @@ -53,9 +51,9 @@ export class GoogleOauthGuard extends AuthGuard('google') { this.guardRedirectService.dispatchErrorFromGuard( context, err, - workspace ?? { - subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), - }, + this.guardRedirectService.getSubdomainAndHostnameFromWorkspace( + workspace, + ), ); return false; diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-provider-enabled.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-provider-enabled.guard.ts index 7ca9fc98f..17e9e93e2 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-provider-enabled.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-provider-enabled.guard.ts @@ -28,9 +28,11 @@ export class GoogleProviderEnabledGuard implements CanActivate { return true; } catch (err) { - this.guardRedirectService.dispatchErrorFromGuard(context, err, { - subdomain: this.guardRedirectService.getSubdomainFromContext(context), - }); + this.guardRedirectService.dispatchErrorFromGuard( + context, + err, + this.guardRedirectService.getSubdomainAndHostnameFromContext(context), + ); return false; } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-exchange-code-for-token.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-exchange-code-for-token.guard.ts index 7892f8f8c..582577b8b 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-exchange-code-for-token.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-exchange-code-for-token.guard.ts @@ -57,9 +57,7 @@ export class MicrosoftAPIsOauthExchangeCodeForTokenGuard extends AuthGuard( AuthExceptionCode.INSUFFICIENT_SCOPES, ) : error, - { - subdomain: this.guardRedirectService.getSubdomainFromContext(context), - }, + this.guardRedirectService.getSubdomainAndHostnameFromContext(context), ); return false; diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-request-code.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-request-code.guard.ts index 5f5fc8a97..92f02c131 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-request-code.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-request-code.guard.ts @@ -72,9 +72,9 @@ export class MicrosoftAPIsOauthRequestCodeGuard extends AuthGuard( this.guardRedirectService.dispatchErrorFromGuard( context, err, - workspace ?? { - subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), - }, + this.guardRedirectService.getSubdomainAndHostnameFromWorkspace( + workspace, + ), ); return false; diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts index 58c1a2d59..dd233a1f2 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts @@ -5,14 +5,12 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @Injectable() export class MicrosoftOAuthGuard extends AuthGuard('microsoft') { constructor( private readonly guardRedirectService: GuardRedirectService, - private readonly environmentService: EnvironmentService, @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, ) { @@ -41,9 +39,9 @@ export class MicrosoftOAuthGuard extends AuthGuard('microsoft') { this.guardRedirectService.dispatchErrorFromGuard( context, err, - workspace ?? { - subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), - }, + this.guardRedirectService.getSubdomainAndHostnameFromWorkspace( + workspace, + ), ); return false; diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard.ts index 48a3d549e..3f359f0c2 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard.ts @@ -28,9 +28,11 @@ export class MicrosoftProviderEnabledGuard implements CanActivate { return true; } catch (err) { - this.guardRedirectService.dispatchErrorFromGuard(context, err, { - subdomain: this.guardRedirectService.getSubdomainFromContext(context), - }); + this.guardRedirectService.dispatchErrorFromGuard( + context, + err, + this.guardRedirectService.getSubdomainAndHostnameFromContext(context), + ); return false; } diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts index 69a1dd59a..2ae7118cf 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts @@ -12,7 +12,6 @@ import { import { OIDCAuthStrategy } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy'; 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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; @@ -21,12 +20,13 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') { constructor( private readonly sSOService: SSOService, private readonly guardRedirectService: GuardRedirectService, - private readonly environmentService: EnvironmentService, ) { super(); } - private getIdentityProviderId(request: any): string { + private getStateByRequest(request: any): { + identityProviderId: string; + } { if (request.params.identityProviderId) { return request.params.identityProviderId; } @@ -39,24 +39,27 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') { ) { const state = JSON.parse(request.query.state); - return state.identityProviderId; + return { + identityProviderId: state.identityProviderId, + }; } throw new Error('Invalid OIDC identity provider params'); } async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); + const request = context.switchToHttp().getRequest(); let identityProvider: | (SSOConfiguration & WorkspaceSSOIdentityProvider) | null = null; try { - const identityProviderId = this.getIdentityProviderId(request); + const state = this.getStateByRequest(request); - identityProvider = - await this.sSOService.findSSOIdentityProviderById(identityProviderId); + identityProvider = await this.sSOService.findSSOIdentityProviderById( + state.identityProviderId, + ); if (!identityProvider) { throw new AuthException( @@ -77,9 +80,9 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') { this.guardRedirectService.dispatchErrorFromGuard( context, err, - identityProvider?.workspace ?? { - subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), - }, + this.guardRedirectService.getSubdomainAndHostnameFromWorkspace( + identityProvider?.workspace, + ), ); return false; diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts index b5aaec503..b512ace2e 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts @@ -3,6 +3,8 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { Request } from 'express'; + import { AuthException, AuthExceptionCode, @@ -10,22 +12,34 @@ import { import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy'; 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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; +import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; @Injectable() export class SAMLAuthGuard extends AuthGuard('saml') { constructor( private readonly sSOService: SSOService, private readonly guardRedirectService: GuardRedirectService, - private readonly environmentService: EnvironmentService, + private readonly exceptionHandlerService: ExceptionHandlerService, ) { super(); } + private getRelayStateByRequest(request: Request) { + try { + const relayStateRaw = request.body.RelayState || request.query.RelayState; + + if (relayStateRaw) { + return JSON.parse(relayStateRaw); + } + } catch (error) { + this.exceptionHandlerService.captureExceptions(error); + } + } + async canActivate(context: ExecutionContext) { - const request = context.switchToHttp().getRequest(); + const request = context.switchToHttp().getRequest(); let identityProvider: | (SSOConfiguration & WorkspaceSSOIdentityProvider) @@ -49,9 +63,9 @@ export class SAMLAuthGuard extends AuthGuard('saml') { this.guardRedirectService.dispatchErrorFromGuard( context, err, - identityProvider?.workspace ?? { - subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), - }, + this.guardRedirectService.getSubdomainAndHostnameFromWorkspace( + identityProvider?.workspace, + ), ); return false; diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index d5ba7fcad..4779f019c 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -455,15 +455,15 @@ export class AuthService { computeRedirectURI({ loginToken, - subdomain, + workspace, billingCheckoutSessionState, }: { loginToken: string; - subdomain: string; + workspace: Pick; billingCheckoutSessionState?: string; }) { const url = this.domainManagerService.buildWorkspaceURL({ - subdomain, + workspace, pathname: '/verify', searchParams: { loginToken, diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/oidc.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/oidc.auth.strategy.ts index 42f37fd32..0f3bfb80a 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/oidc.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/oidc.auth.strategy.ts @@ -50,7 +50,6 @@ export class OIDCAuthStrategy extends PassportStrategy( ...options, state: JSON.stringify({ identityProviderId: req.params.identityProviderId, - ...(req.query.forceSubdomainUrl ? { forceSubdomainUrl: true } : {}), ...(req.query.workspaceInviteHash ? { workspaceInviteHash: req.query.workspaceInviteHash } : {}), diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts index 79e439d2e..c946fe599 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-entitlement-key.enum.ts @@ -2,4 +2,5 @@ export enum BillingEntitlementKey { SSO = 'SSO', + CUSTOM_DOMAIN = 'CUSTOM_DOMAIN', } diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts index 27aa80998..d6ff677c0 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts @@ -47,7 +47,7 @@ export class BillingPortalWorkspaceService { requirePaymentMethod, }: BillingPortalCheckoutSessionParameters): Promise { const frontBaseUrl = this.domainManagerService.buildWorkspaceURL({ - subdomain: workspace.subdomain, + workspace, }); const cancelUrl = frontBaseUrl.toString(); @@ -118,7 +118,7 @@ export class BillingPortalWorkspaceService { } const frontBaseUrl = this.domainManagerService.buildWorkspaceURL({ - subdomain: workspace.subdomain, + workspace, }); if (returnUrlPath) { diff --git a/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-entitlement-updated-event-to-database-entitlement.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-entitlement-updated-event-to-database-entitlement.util.spec.ts index 302309428..cd11c11c7 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-entitlement-updated-event-to-database-entitlement.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-entitlement-updated-event-to-database-entitlement.util.spec.ts @@ -41,6 +41,12 @@ describe('transformStripeEntitlementUpdatedEventToDatabaseEntitlement', () => { value: true, stripeCustomerId: 'cus_123', }, + { + key: BillingEntitlementKey.CUSTOM_DOMAIN, + stripeCustomerId: 'cus_123', + value: false, + workspaceId: 'workspaceId', + }, ]); }); @@ -79,6 +85,12 @@ describe('transformStripeEntitlementUpdatedEventToDatabaseEntitlement', () => { value: false, stripeCustomerId: 'cus_123', }, + { + key: 'CUSTOM_DOMAIN', + stripeCustomerId: 'cus_123', + value: false, + workspaceId: 'workspaceId', + }, ]); }); }); diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.spec.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.spec.ts index e8c8b167c..c8d4165c8 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.spec.ts @@ -9,6 +9,53 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DomainManagerService } from './domain-manager.service'; describe('DomainManagerService', () => { + describe('getworkspaceUrls', () => { + it('should return a URL containing the correct hostname if hostname is provided', () => { + jest + .spyOn(environmentService, 'get') + .mockImplementation((key: string) => { + const env = { + FRONT_PROTOCOL: 'https', + FRONT_DOMAIN: 'example.com', + }; + + return env[key]; + }); + + const result = domainManagerService.getworkspaceUrls({ + subdomain: 'subdomain', + hostname: 'custom-host.com', + }); + + expect(result).toEqual({ + customUrl: 'https://custom-host.com/', + subdomainUrl: 'https://subdomain.example.com/', + }); + }); + + it('should return a URL containing the correct subdomain if hostname is not provided but subdomain is', () => { + jest + .spyOn(environmentService, 'get') + .mockImplementation((key: string) => { + const env = { + FRONT_PROTOCOL: 'https', + FRONT_DOMAIN: 'example.com', + }; + + return env[key]; + }); + + const result = domainManagerService.getworkspaceUrls({ + subdomain: 'subdomain', + hostname: undefined, + }); + + expect(result).toEqual({ + customUrl: undefined, + subdomainUrl: 'https://subdomain.example.com/', + }); + }); + }); let domainManagerService: DomainManagerService; let environmentService: EnvironmentService; @@ -106,7 +153,10 @@ describe('DomainManagerService', () => { }); const result = domainManagerService.buildWorkspaceURL({ - subdomain: 'test', + workspace: { + subdomain: 'test', + hostname: undefined, + }, }); expect(result.toString()).toBe('https://test.example.com/'); @@ -125,7 +175,10 @@ describe('DomainManagerService', () => { }); const result = domainManagerService.buildWorkspaceURL({ - subdomain: 'subdomain', + workspace: { + subdomain: 'test', + hostname: undefined, + }, pathname: '/path/to/resource', }); @@ -145,8 +198,10 @@ describe('DomainManagerService', () => { }); const result = domainManagerService.buildWorkspaceURL({ - subdomain: 'subdomain', - + workspace: { + subdomain: 'test', + hostname: undefined, + }, searchParams: { foo: 'bar', baz: 123, diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts index 6f51204f0..6532dc970 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts +++ b/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts @@ -74,33 +74,31 @@ export class DomainManagerService { buildEmailVerificationURL({ emailVerificationToken, email, - subdomain, + workspace, }: { emailVerificationToken: string; email: string; - subdomain: string; + workspace: Pick; }) { return this.buildWorkspaceURL({ - subdomain, + workspace, pathname: 'verify-email', searchParams: { emailVerificationToken, email }, }); } buildWorkspaceURL({ - subdomain, + workspace, pathname, searchParams, }: { - subdomain: string; + workspace: Pick; pathname?: string; searchParams?: Record; }) { - const url = this.getFrontUrl(); + const workspaceUrls = this.getworkspaceUrls(workspace); - if (this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) { - url.hostname = `${subdomain}.${url.hostname}`; - } + const url = new URL(workspaceUrls.customUrl ?? workspaceUrls.subdomainUrl); if (pathname) { url.pathname = pathname; @@ -117,21 +115,6 @@ export class DomainManagerService { return url; } - // @Deprecated - getWorkspaceSubdomainFromUrl = (url: string) => { - const { hostname: originHostname } = new URL(url); - - if (!originHostname.endsWith(this.getFrontUrl().hostname)) { - return null; - } - - const frontDomain = this.getFrontUrl().hostname; - - const subdomain = originHostname.replace(`.${frontDomain}`, ''); - - return this.isDefaultSubdomain(subdomain) ? null : subdomain; - }; - getSubdomainAndHostnameFromUrl = (url: string) => { const { hostname: originHostname } = new URL(url); @@ -162,9 +145,12 @@ export class DomainManagerService { return subdomain === this.environmentService.get('DEFAULT_SUBDOMAIN'); } - computeRedirectErrorUrl(errorMessage: string, subdomain: string) { + computeRedirectErrorUrl( + errorMessage: string, + workspace: Pick, + ) { const url = this.buildWorkspaceURL({ - subdomain: subdomain, + workspace, pathname: '/verify', searchParams: { errorMessage }, }); @@ -352,7 +338,7 @@ export class DomainManagerService { await this.deleteCustomHostname(fromCustomHostname.id); } - return await this.registerCustomHostname(toHostname); + return this.registerCustomHostname(toHostname); } async deleteCustomHostnameByHostnameSilently(hostname: string) { @@ -378,4 +364,32 @@ export class DomainManagerService { zone_id: this.environmentService.get('CLOUDFLARE_ZONE_ID'), }); } + + private getCustomWorkspaceEndpoint(hostname: string) { + const url = this.getFrontUrl(); + + url.hostname = hostname; + + return url.toString(); + } + + private getTwentyWorkspaceEndpoint(subdomain: string) { + const url = this.getFrontUrl(); + + url.hostname = `${subdomain}.${url.hostname}`; + + return url.toString(); + } + + getworkspaceUrls({ + subdomain, + hostname, + }: Pick) { + return { + customUrl: hostname + ? this.getCustomWorkspaceEndpoint(hostname) + : undefined, + subdomainUrl: this.getTwentyWorkspaceEndpoint(subdomain), + }; + } } diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts index 1a22a62e4..1484581a1 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts @@ -29,7 +29,7 @@ export class EmailVerificationResolver { return await this.emailVerificationService.resendEmailVerificationToken( resendEmailVerificationTokenInput.email, - workspace.subdomain, + workspace, ); } } diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts b/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts index 31c424656..96578532b 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts @@ -21,6 +21,7 @@ import { import { EmailService } from 'src/engine/core-modules/email/email.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @Injectable() // eslint-disable-next-line @nx/workspace-inject-workspace-repository @@ -38,7 +39,7 @@ export class EmailVerificationService { async sendVerificationEmail( userId: string, email: string, - subdomain: string, + workspace: Pick, ) { if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) { return { success: false }; @@ -51,7 +52,7 @@ export class EmailVerificationService { this.domainManagerService.buildEmailVerificationURL({ emailVerificationToken, email, - subdomain, + workspace, }); const emailData = { @@ -80,7 +81,10 @@ export class EmailVerificationService { return { success: true }; } - async resendEmailVerificationToken(email: string, subdomain: string) { + async resendEmailVerificationToken( + email: string, + workspace: Pick, + ) { if (!this.environmentService.get('IS_EMAIL_VERIFICATION_REQUIRED')) { throw new EmailVerificationException( 'Email verification token cannot be sent because email verification is not required', @@ -121,7 +125,7 @@ export class EmailVerificationService { await this.appTokenRepository.delete(existingToken.id); } - await this.sendVerificationEmail(user.id, email, subdomain); + await this.sendVerificationEmail(user.id, email, workspace); return { success: true }; } diff --git a/packages/twenty-server/src/engine/core-modules/guard-redirect/services/guard-redirect.service.ts b/packages/twenty-server/src/engine/core-modules/guard-redirect/services/guard-redirect.service.ts index 94e9f2564..b6a9fff6a 100644 --- a/packages/twenty-server/src/engine/core-modules/guard-redirect/services/guard-redirect.service.ts +++ b/packages/twenty-server/src/engine/core-modules/guard-redirect/services/guard-redirect.service.ts @@ -1,10 +1,13 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; +import { Request } from 'express'; + import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { CustomException } from 'src/utils/custom-exception'; import { AuthException } from 'src/engine/core-modules/auth/auth.exception'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @Injectable() export class GuardRedirectService { @@ -17,7 +20,7 @@ export class GuardRedirectService { dispatchErrorFromGuard( context: ExecutionContext, error: Error | CustomException, - workspace: { id?: string; subdomain: string }, + workspace: { id?: string; subdomain: string; hostname?: string }, ) { if ('contextType' in context && context.contextType === 'graphql') { throw error; @@ -29,15 +32,36 @@ export class GuardRedirectService { .redirect(this.getRedirectErrorUrlAndCaptureExceptions(error, workspace)); } - getSubdomainFromContext(context: ExecutionContext) { - const request = context.switchToHttp().getRequest(); + getSubdomainAndHostnameFromWorkspace( + workspace?: Pick | null, + ) { + if (!workspace) { + return { + subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'), + }; + } - const subdomainFromUrl = - this.domainManagerService.getWorkspaceSubdomainFromUrl( - request.headers.referer, - ); + return workspace; + } - return subdomainFromUrl ?? this.environmentService.get('DEFAULT_SUBDOMAIN'); + getSubdomainAndHostnameFromContext(context: ExecutionContext): { + subdomain: string; + hostname?: string; + } { + const request = context.switchToHttp().getRequest(); + + const subdomainAndHostnameFromReferer = request.headers.referer + ? this.domainManagerService.getSubdomainAndHostnameFromUrl( + request.headers.referer, + ) + : null; + + return { + subdomain: + subdomainAndHostnameFromReferer?.subdomain ?? + this.environmentService.get('DEFAULT_SUBDOMAIN'), + hostname: subdomainAndHostnameFromReferer?.hostname, + }; } private captureException(err: Error | CustomException, workspaceId?: string) { @@ -52,13 +76,13 @@ export class GuardRedirectService { getRedirectErrorUrlAndCaptureExceptions( err: Error | CustomException, - workspace: { id?: string; subdomain: string }, + workspace: { id?: string; subdomain: string; hostname?: string }, ) { this.captureException(err, workspace.id); return this.domainManagerService.computeRedirectErrorUrl( err instanceof AuthException ? err.message : 'Unknown error', - workspace.subdomain, + workspace, ); } } diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.input.ts b/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.input.ts index e627a4a7e..870dbb72b 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.input.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.input.ts @@ -2,7 +2,7 @@ import { Field, InputType } from '@nestjs/graphql'; -import { IsOptional, IsString } from 'class-validator'; +import { IsOptional, IsBoolean, IsString } from 'class-validator'; @InputType() export class GetAuthorizationUrlInput { diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts index 0a854a26a..71bec0a35 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts @@ -15,6 +15,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; +import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; @Module({ imports: [ @@ -29,6 +30,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works DataSourceModule, WorkspaceDataSourceModule, WorkspaceInvitationModule, + DomainManagerModule, TwentyORMModule, ], services: [UserWorkspaceService], diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index 4e9d18f5f..cb8b5dce9 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -24,6 +24,7 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global. import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { assert } from 'src/utils/assert'; +import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; export class UserWorkspaceService extends TypeOrmQueryService { constructor( @@ -37,6 +38,7 @@ export class UserWorkspaceService extends TypeOrmQueryService { private readonly typeORMService: TypeORMService, private readonly workspaceInvitationService: WorkspaceInvitationService, private readonly workspaceEventEmitter: WorkspaceEventEmitter, + private readonly domainManagerService: DomainManagerService, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) { super(userWorkspaceRepository); @@ -179,7 +181,9 @@ export class UserWorkspaceService extends TypeOrmQueryService { return user.workspaces.map((userWorkspace) => ({ id: userWorkspace.workspaceId, displayName: userWorkspace.workspace.displayName, - subdomain: userWorkspace.workspace.subdomain, + workspaceUrls: this.domainManagerService.getworkspaceUrls( + userWorkspace.workspace, + ), logo: userWorkspace.workspace.logo, sso: userWorkspace.workspace.workspaceSSOIdentityProviders.reduce( (acc, identityProvider) => diff --git a/packages/twenty-server/src/engine/core-modules/user/user.entity.ts b/packages/twenty-server/src/engine/core-modules/user/user.entity.ts index c6e696dfe..565eaec4e 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.entity.ts @@ -19,7 +19,6 @@ import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-p import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; registerEnumType(OnboardingStatus, { name: 'OnboardingStatus', @@ -100,7 +99,4 @@ export class User { @Field(() => OnboardingStatus, { nullable: true }) onboardingStatus: OnboardingStatus; - - @Field(() => Workspace, { nullable: true }) - currentWorkspace: Relation; } diff --git a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts index b075c0f91..4b05f4774 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts @@ -81,22 +81,7 @@ export class UserResolver { ) {} @Query(() => User) - async currentUser( - @AuthUser() { id: userId }: User, - @OriginHeader() origin: string, - ): Promise { - const workspace = - await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( - origin, - ); - - workspaceValidator.assertIsDefinedOrThrow(workspace); - - await this.userService.hasUserAccessToWorkspaceOrThrow( - userId, - workspace.id, - ); - + async currentUser(@AuthUser() { id: userId }: User): Promise { const user = await this.userRepository.findOne({ where: { id: userId, @@ -109,7 +94,7 @@ export class UserResolver { new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND), ); - return { ...user, currentWorkspace: workspace }; + return user; } @ResolveField(() => GraphQLJSONObject) @@ -314,4 +299,9 @@ export class UserResolver { return this.onboardingService.getOnboardingStatus(user, workspace); } + + @ResolveField(() => Workspace) + async currentWorkspace(@AuthWorkspace() workspace: Workspace) { + return workspace; + } } diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts index c7c731287..a76fc5943 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts @@ -279,7 +279,7 @@ export class WorkspaceInvitationService { for (const invitation of invitationsPr) { if (invitation.status === 'fulfilled') { const link = this.domainManagerService.buildWorkspaceURL({ - subdomain: workspace.subdomain, + workspace, pathname: `invite/${workspace?.inviteHash}`, searchParams: invitation.value.isPersonalInvitation ? { diff --git a/packages/twenty-server/src/engine/core-modules/workspace/dtos/public-workspace-data-output.ts b/packages/twenty-server/src/engine/core-modules/workspace/dtos/public-workspace-data-output.ts index 9b6fa8c21..a72c6d8cc 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/dtos/public-workspace-data-output.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/dtos/public-workspace-data-output.ts @@ -5,6 +5,7 @@ import { IdentityProviderType, SSOIdentityProviderStatus, } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; +import { workspaceUrls } from 'src/engine/core-modules/workspace/dtos/workspace-endpoints.dto'; @ObjectType() export class SSOIdentityProvider { @@ -56,9 +57,6 @@ export class PublicWorkspaceDataOutput { @Field(() => String, { nullable: true }) displayName: Workspace['displayName']; - @Field(() => String) - subdomain: Workspace['subdomain']; - - @Field(() => String, { nullable: true }) - hostname: Workspace['hostname']; + @Field(() => workspaceUrls) + workspaceUrls: workspaceUrls; } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/dtos/workspace-endpoints.dto.ts b/packages/twenty-server/src/engine/core-modules/workspace/dtos/workspace-endpoints.dto.ts new file mode 100644 index 000000000..714dd0230 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workspace/dtos/workspace-endpoints.dto.ts @@ -0,0 +1,10 @@ +import { ObjectType, Field } from '@nestjs/graphql'; + +@ObjectType() +export class workspaceUrls { + @Field(() => String, { nullable: true }) + customUrl?: string; + + @Field(() => String) + subdomainUrl: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto.ts b/packages/twenty-server/src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto.ts index b35d4ba4b..b6286a88e 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/dtos/workspace-subdomain-id.dto.ts @@ -1,9 +1,11 @@ import { Field, ObjectType } from '@nestjs/graphql'; +import { workspaceUrls } from 'src/engine/core-modules/workspace/dtos/workspace-endpoints.dto'; + @ObjectType() -export class WorkspaceSubdomainAndId { - @Field() - subdomain: string; +export class workspaceUrlsAndId { + @Field(() => workspaceUrls) + workspaceUrls: workspaceUrls; @Field() id: string; diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts index 2e5020e55..d031f0f1b 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts @@ -15,6 +15,7 @@ import { User } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; +import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { WorkspaceService } from './workspace.service'; @@ -81,6 +82,10 @@ describe('WorkspaceService', () => { provide: FeatureFlagService, useValue: {}, }, + { + provide: ExceptionHandlerService, + useValue: {}, + }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts index 194096f8b..3b6d3a11a 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts @@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import assert from 'assert'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; -import { WorkspaceActivationStatus, isDefined } from 'twenty-shared'; +import { isDefined, WorkspaceActivationStatus } from 'twenty-shared'; import { Repository } from 'typeorm'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; @@ -24,10 +24,14 @@ import { import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags'; +import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; +import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; @Injectable() // eslint-disable-next-line @nx/workspace-inject-workspace-repository export class WorkspaceService extends TypeOrmQueryService { + private readonly featureLookUpKey = BillingEntitlementKey.CUSTOM_DOMAIN; + constructor( @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, @@ -42,10 +46,26 @@ export class WorkspaceService extends TypeOrmQueryService { private readonly userWorkspaceService: UserWorkspaceService, private readonly environmentService: EnvironmentService, private readonly domainManagerService: DomainManagerService, + private readonly exceptionHandlerService: ExceptionHandlerService, ) { 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); @@ -61,6 +81,8 @@ export class WorkspaceService extends TypeOrmQueryService { } private async setCustomDomain(workspace: Workspace, hostname: string) { + await this.isCustomDomainEnabled(workspace.id); + const existingWorkspace = await this.workspaceRepository.findOne({ where: { hostname }, }); @@ -126,10 +148,11 @@ export class WorkspaceService extends TypeOrmQueryService { if (payload.hostname && customDomainRegistered) { this.domainManagerService .deleteCustomHostnameByHostnameSilently(payload.hostname) - .catch(() => { - // send to sentry + .catch((err) => { + this.exceptionHandlerService.captureExceptions([err]); }); } + throw error; } } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.exception.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.exception.ts index 933a4fee9..6c91d2ef1 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.exception.ts @@ -11,4 +11,5 @@ export enum WorkspaceExceptionCode { SUBDOMAIN_ALREADY_TAKEN = 'SUBDOMAIN_ALREADY_TAKEN', DOMAIN_ALREADY_TAKEN = 'DOMAIN_ALREADY_TAKEN', WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND', + WORKSPACE_CUSTOM_DOMAIN_DISABLED = 'WORKSPACE_CUSTOM_DOMAIN_DISABLED', } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index 833c68e18..6305579db 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -17,7 +17,6 @@ import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder. import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; -import { CustomHostnameDetails } from 'src/engine/core-modules/domain-manager/dtos/custom-hostname-details'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; @@ -44,6 +43,8 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { GraphqlValidationExceptionFilter } from 'src/filters/graphql-validation-exception.filter'; import { assert } from 'src/utils/assert'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; +import { CustomHostnameDetails } from 'src/engine/core-modules/domain-manager/dtos/custom-hostname-details'; +import { workspaceUrls } from 'src/engine/core-modules/workspace/dtos/workspace-endpoints.dto'; import { Workspace } from './workspace.entity'; @@ -214,6 +215,11 @@ export class WorkspaceResolver { return isDefined(this.environmentService.get('ENTERPRISE_KEY')); } + @ResolveField(() => workspaceUrls) + workspaceUrls(@Parent() workspace: Workspace) { + return this.domainManagerService.getworkspaceUrls(workspace); + } + @Query(() => CustomHostnameDetails, { nullable: true }) @UseGuards(WorkspaceAuthGuard) async getHostnameDetails( @@ -225,7 +231,7 @@ export class WorkspaceResolver { } @Query(() => PublicWorkspaceDataOutput) - async getPublicWorkspaceDataBySubdomain( + async getPublicWorkspaceDataByDomain( @OriginHeader() origin: string, ): Promise { try { @@ -262,8 +268,7 @@ export class WorkspaceResolver { id: workspace.id, logo: workspaceLogoWithToken, displayName: workspace.displayName, - subdomain: workspace.subdomain, - hostname: workspace.hostname, + workspaceUrls: this.domainManagerService.getworkspaceUrls(workspace), authProviders: getAuthProvidersByWorkspace({ workspace, systemEnabledProviders, diff --git a/packages/twenty-server/src/engine/middlewares/constants/excluded-middleware-operations.constant.ts b/packages/twenty-server/src/engine/middlewares/constants/excluded-middleware-operations.constant.ts index fc5f340af..1604611d9 100644 --- a/packages/twenty-server/src/engine/middlewares/constants/excluded-middleware-operations.constant.ts +++ b/packages/twenty-server/src/engine/middlewares/constants/excluded-middleware-operations.constant.ts @@ -15,5 +15,5 @@ export const EXCLUDED_MIDDLEWARE_OPERATIONS = [ 'IntrospectionQuery', 'ExchangeAuthorizationCode', 'GetAuthorizationUrl', - 'GetPublicWorkspaceDataBySubdomain', + 'GetPublicWorkspaceDataByDomain', ] as const;