feat(): enable custom domain usage (#9911)

# Content
- Introduce the `workspaceUrls` property. It contains two
sub-properties: `customUrl, subdomainUrl`. These endpoints are used to
access the workspace. Even if the `workspaceUrls` is invalid for
multiple reasons, the `subdomainUrl` remains valid.
- Introduce `ResolveField` workspaceEndpoints to avoid unnecessary URL
computation on the frontend part.
- Add a `forceSubdomainUrl` to avoid custom URL using a query parameter
This commit is contained in:
Antoine Moreaux
2025-02-07 14:34:26 +01:00
committed by GitHub
parent 3cc66fe712
commit 68183b7c85
87 changed files with 645 additions and 373 deletions

View File

@ -113,7 +113,7 @@ export type AvailableWorkspaceOutput = {
id: Scalars['String']['output'];
logo?: Maybe<Scalars['String']['output']>;
sso: Array<SsoConnection>;
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<Scalars['String']['output']>;
hostname?: Maybe<Scalars['String']['output']>;
id: Scalars['String']['output'];
logo?: Maybe<Scalars['String']['output']>;
subdomain: Scalars['String']['output'];
workspaceUrls: WorkspaceUrls;
};
export type PublishServerlessFunctionInput = {
@ -1394,7 +1385,7 @@ export type Query = {
getHostnameDetails?: Maybe<CustomHostnameDetails>;
getPostgresCredentials?: Maybe<PostgresCredentials>;
getProductPrices: BillingProductPricesOutput;
getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput;
getPublicWorkspaceDataByDomain: PublicWorkspaceDataOutput;
getRoles: Array<Role>;
getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']['output']>;
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<AnalyticsTinybirdJwtMap>;
canImpersonate: Scalars['Boolean']['output'];
createdAt: Scalars['DateTime']['output'];
currentWorkspace?: Maybe<Workspace>;
currentWorkspace: Workspace;
defaultAvatarUrl?: Maybe<Scalars['String']['output']>;
deletedAt?: Maybe<Scalars['DateTime']['output']>;
disabled?: Maybe<Scalars['Boolean']['output']>;
@ -2126,6 +2117,7 @@ export type Workspace = {
subdomain: Scalars['String']['output'];
updatedAt: Scalars['DateTime']['output'];
workspaceMembersCount?: Maybe<Scalars['Float']['output']>;
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<Scalars['String']['output']>;
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 };

View File

@ -106,7 +106,7 @@ export type AvailableWorkspaceOutput = {
id: Scalars['String'];
logo?: Maybe<Scalars['String']>;
sso: Array<SsoConnection>;
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<Scalars['String']>;
hostname?: Maybe<Scalars['String']>;
id: Scalars['String'];
logo?: Maybe<Scalars['String']>;
subdomain: Scalars['String'];
workspaceUrls: WorkspaceUrls;
};
export type PublishServerlessFunctionInput = {
@ -1258,7 +1249,7 @@ export type Query = {
getHostnameDetails?: Maybe<CustomHostnameDetails>;
getPostgresCredentials?: Maybe<PostgresCredentials>;
getProductPrices: BillingProductPricesOutput;
getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput;
getPublicWorkspaceDataByDomain: PublicWorkspaceDataOutput;
getRoles: Array<Role>;
getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']>;
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<AnalyticsTinybirdJwtMap>;
canImpersonate: Scalars['Boolean'];
createdAt: Scalars['DateTime'];
currentWorkspace?: Maybe<Workspace>;
currentWorkspace: Workspace;
defaultAvatarUrl?: Maybe<Scalars['String']>;
deletedAt?: Maybe<Scalars['DateTime']>;
disabled?: Maybe<Scalars['Boolean']>;
@ -1904,6 +1895,7 @@ export type Workspace = {
subdomain: Scalars['String'];
updatedAt: Scalars['DateTime'];
workspaceMembersCount?: Maybe<Scalars['Float']>;
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<Scalars['String']>;
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<typeof useCheckUserExistsQuery>;
export type CheckUserExistsLazyQueryHookResult = ReturnType<typeof useCheckUserExistsLazyQuery>;
export type CheckUserExistsQueryResult = Apollo.QueryResult<CheckUserExistsQuery, CheckUserExistsQueryVariables>;
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<GetPublicWorkspaceDataBySubdomainQuery, GetPublicWorkspaceDataBySubdomainQueryVariables>) {
export function useGetPublicWorkspaceDataByDomainQuery(baseOptions?: Apollo.QueryHookOptions<GetPublicWorkspaceDataByDomainQuery, GetPublicWorkspaceDataByDomainQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetPublicWorkspaceDataBySubdomainQuery, GetPublicWorkspaceDataBySubdomainQueryVariables>(GetPublicWorkspaceDataBySubdomainDocument, options);
return Apollo.useQuery<GetPublicWorkspaceDataByDomainQuery, GetPublicWorkspaceDataByDomainQueryVariables>(GetPublicWorkspaceDataByDomainDocument, options);
}
export function useGetPublicWorkspaceDataBySubdomainLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetPublicWorkspaceDataBySubdomainQuery, GetPublicWorkspaceDataBySubdomainQueryVariables>) {
export function useGetPublicWorkspaceDataByDomainLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetPublicWorkspaceDataByDomainQuery, GetPublicWorkspaceDataByDomainQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetPublicWorkspaceDataBySubdomainQuery, GetPublicWorkspaceDataBySubdomainQueryVariables>(GetPublicWorkspaceDataBySubdomainDocument, options);
return Apollo.useLazyQuery<GetPublicWorkspaceDataByDomainQuery, GetPublicWorkspaceDataByDomainQueryVariables>(GetPublicWorkspaceDataByDomainDocument, options);
}
export type GetPublicWorkspaceDataBySubdomainQueryHookResult = ReturnType<typeof useGetPublicWorkspaceDataBySubdomainQuery>;
export type GetPublicWorkspaceDataBySubdomainLazyQueryHookResult = ReturnType<typeof useGetPublicWorkspaceDataBySubdomainLazyQuery>;
export type GetPublicWorkspaceDataBySubdomainQueryResult = Apollo.QueryResult<GetPublicWorkspaceDataBySubdomainQuery, GetPublicWorkspaceDataBySubdomainQueryVariables>;
export type GetPublicWorkspaceDataByDomainQueryHookResult = ReturnType<typeof useGetPublicWorkspaceDataByDomainQuery>;
export type GetPublicWorkspaceDataByDomainLazyQueryHookResult = ReturnType<typeof useGetPublicWorkspaceDataByDomainLazyQuery>;
export type GetPublicWorkspaceDataByDomainQueryResult = Apollo.QueryResult<GetPublicWorkspaceDataByDomainQuery, GetPublicWorkspaceDataByDomainQueryVariables>;
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
}
}
`;

View File

@ -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: {

View File

@ -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 {

View File

@ -22,7 +22,10 @@ export const SIGN_UP = gql`
}
workspace {
id
subdomain
workspaceUrls {
subdomainUrl
customUrl
}
}
}
}

View File

@ -9,8 +9,10 @@ export const CHECK_USER_EXISTS = gql`
availableWorkspaces {
id
displayName
subdomain
hostname
workspaceUrls {
subdomainUrl
customUrl
}
logo
sso {
type

View File

@ -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

View File

@ -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 && {

View File

@ -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') {

View File

@ -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 }) => (
<MockedProvider mocks={apolloMocks} addTypename={false}>
{children}
</MockedProvider>
<MemoryRouter>
<MockedProvider mocks={apolloMocks} addTypename={false}>
{children}
</MockedProvider>
</MemoryRouter>
);
describe('useSSO', () => {

View File

@ -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) {

View File

@ -21,6 +21,7 @@ export type CurrentWorkspace = Pick<
| 'hasValidEnterpriseKey'
| 'subdomain'
| 'hostname'
| 'workspaceUrls'
| 'metadataVersion'
>;

View File

@ -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<Workspaces>({

View File

@ -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<string, string>,
searchParams?: Record<string, string | boolean>,
) => {
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();

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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 : {}),

View File

@ -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,
};
};

View File

@ -0,0 +1,11 @@
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
export const useReadWorkspaceUrlFromCurrentLocation = () => {
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
return {
currentLocationHostname: isOnAWorkspace
? window.location.hostname
: undefined,
};
};

View File

@ -9,12 +9,12 @@ export const useRedirectToWorkspaceDomain = () => {
const { redirect } = useRedirect();
const redirectToWorkspaceDomain = (
subdomain: string,
baseUrl: string,
pathname?: string,
searchParams?: Record<string, string>,
searchParams?: Record<string, string | boolean>,
) => {
if (!isMultiWorkspaceEnabled) return;
redirect(buildWorkspaceUrl(subdomain, pathname, searchParams));
redirect(buildWorkspaceUrl(baseUrl, pathname, searchParams));
};
return {

View File

@ -3,7 +3,7 @@ import { cookieStorageEffect } from '~/utils/recoil-effects';
export const lastAuthenticatedWorkspaceDomainState = createState<
| {
subdomain: string;
workspaceUrl: string;
workspaceId: string;
cookieAttributes?: Cookies.CookieAttributes;
}

View File

@ -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,

View File

@ -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,

View File

@ -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);

View File

@ -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) => (
<UndecoratedLink
key={workspace.id}
to={buildWorkspaceUrl(workspace.subdomain)}
to={buildWorkspaceUrl(getWorkspaceUrl(workspace.workspaceUrls))}
onClick={(event) => {
event?.preventDefault();
handleChange(workspace);

View File

@ -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

View File

@ -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,

View File

@ -4,7 +4,6 @@ export const ACTIVATE_WORKSPACE = gql`
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
activateWorkspace(data: $input) {
id
subdomain
}
}
`;

View File

@ -7,7 +7,6 @@ export const GET_WORKSPACE_FROM_INVITE_HASH = gql`
displayName
logo
allowImpersonation
subdomain
}
}
`;

View File

@ -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 <SignInUpSSOIdentityProviderSelection />;
@ -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,

View File

@ -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());
},
});
};

View File

@ -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) && (
<pre>
{getHostnameDetailsData.getHostnameDetails.hostname} CNAME
app.twenty-main.com
twenty-main.com
</pre>
)}
{getHostnameDetailsData && (
<pre>{JSON.stringify(getHostnameDetailsData, null, 4)}</pre>
)}
{getHostnameDetailsData?.getHostnameDetails &&
getHostnameDetailsData.getHostnameDetails.ownershipVerifications.map(
(ownershipVerification) => {
if (
ownershipVerification.__typename ===
'CustomHostnameOwnershipVerificationTxt'
) {
return (
<pre>
{ownershipVerification.name} TXT {ownershipVerification.value}
</pre>
);
}
if (
ownershipVerification.__typename ===
'CustomHostnameOwnershipVerificationHttp'
) {
return (
<pre>
{ownershipVerification.url} HTTP {ownershipVerification.body}
</pre>
);
}
return <></>;
},
)}
</Section>
);
};

View File

@ -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,

View File

@ -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: [],

View File

@ -48,6 +48,10 @@ export const mockCurrentWorkspace: Workspace = {
hasValidEnterpriseKey: false,
isGoogleAuthEnabled: true,
isPasswordAuthEnabled: true,
workspaceUrls: {
customUrl: undefined,
subdomainUrl: 'twenty.twenty.com',
},
isMicrosoftAuthEnabled: false,
featureFlags: [
{

View File

@ -0,0 +1,5 @@
import { WorkspaceUrls } from '~/generated/graphql';
export const getWorkspaceUrl = (workspaceUrls: WorkspaceUrls) => {
return workspaceUrls.customUrl ?? workspaceUrls.subdomainUrl;
};