Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(): enable custom domain usage #9911

Merged
merged 61 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
5a69fb3
feat(custom-domain): remove domainName + add migration for custom dom…
AMoreaux Jan 27, 2025
1e1c200
refactor(database): replace hostname migration implementation
AMoreaux Jan 27, 2025
625d722
Merge remote-tracking branch 'origin/main' into feat/add-feature-flag…
AMoreaux Jan 27, 2025
60623a0
refactor(workspace): replace domainName with hostname field
AMoreaux Jan 27, 2025
b3153ed
feat(domain-management): Add custom hostname and subdomain support
AMoreaux Jan 27, 2025
21fa0d1
Merge remote-tracking branch 'origin/main' into feat/add-feature-flag…
AMoreaux Jan 28, 2025
a6000aa
feat(graphql): add support for custom domain feature flag
AMoreaux Jan 28, 2025
ebdc04a
Merge branch 'feat/add-feature-flag-and-migration-for-custom-domain' …
AMoreaux Jan 28, 2025
a5c8798
feat(workspace): Add hostname support across workspace entities
AMoreaux Jan 28, 2025
653960b
refactor: update hostname field and simplify subdomain logic
AMoreaux Jan 28, 2025
a641c0c
feat(graphql): remove `domain` field from UpdateWorkspaceInput
AMoreaux Jan 28, 2025
d7a2919
chore(graphql): remove unused 'domain' field
AMoreaux Jan 28, 2025
a0fe15a
refactor: improve naming and add validation schema
AMoreaux Jan 28, 2025
be7c4f5
Merge remote-tracking branch 'origin/main' into feat/add-feature-flag…
AMoreaux Jan 28, 2025
4e363ef
Merge branch 'feat/add-feature-flag-and-migration-for-custom-domain' …
AMoreaux Jan 28, 2025
8eaf3c8
Merge remote-tracking branch 'origin/main' into feat/allow-to-registe…
AMoreaux Jan 28, 2025
da27cea
feat(graphql): add hostname support and related queries
AMoreaux Jan 28, 2025
0e0ba8a
refactor: update validation and error handling logic
AMoreaux Jan 28, 2025
c766a54
refactor(settings): clean up logging and streamline update logic
AMoreaux Jan 28, 2025
6501f9f
refactor(domain-manager): make DTO fields nullable where needed
AMoreaux Jan 28, 2025
69e6d2d
WIP
AMoreaux Jan 29, 2025
77b1a9d
refactor(domain-manager): unify custom hostname logic
AMoreaux Jan 29, 2025
0188afa
Merge remote-tracking branch 'origin/main' into feat/allow-to-registe…
AMoreaux Jan 29, 2025
ba7a39c
Merge branch 'refs/heads/feat/allow-to-register-a-custom-domain' into…
AMoreaux Jan 29, 2025
98b0baf
refactor(workspace-endpoints): replace workspaceUrl with workspaceEnd…
AMoreaux Jan 29, 2025
1dd89b2
refactor(workspace): replace subdomain usage with workspace object
AMoreaux Jan 29, 2025
f478773
fix: remove insecure TLS rejection override
AMoreaux Jan 29, 2025
a626a0a
feat(workspace): add support for workspace endpoints
AMoreaux Jan 29, 2025
8f35cc1
WIP
AMoreaux Jan 29, 2025
3ff6c49
Merge remote-tracking branch 'origin/main' into feat/enable-custom-do…
AMoreaux Jan 30, 2025
b95ae35
refactor(workspace): unify workspace URLs handling
AMoreaux Jan 30, 2025
0fe28b9
refactor(domain-manager): fix type casing in hook function
AMoreaux Jan 30, 2025
f7b8f9e
Merge branch 'main' into feat/enable-custom-domain-usage
AMoreaux Jan 30, 2025
0c16657
refactor(workspace): extract hostname logic for reuse
AMoreaux Jan 30, 2025
b70d984
feat(auth/sso): Add support for forceSubdomainUrl functionality
AMoreaux Jan 31, 2025
a1d5eb6
Merge remote-tracking branch 'origin/main' into feat/enable-custom-do…
AMoreaux Jan 31, 2025
89185de
feat(security): add support for Microsoft SSO icon detection
AMoreaux Jan 31, 2025
a9fe608
Merge remote-tracking branch 'origin/main' into feat/enable-custom-do…
AMoreaux Jan 31, 2025
1e7e1b6
refactor(auth): update dependency array in useAuth hook
AMoreaux Jan 31, 2025
aa02958
Merge remote-tracking branch 'origin/main' into feat/enable-custom-do…
AMoreaux Jan 31, 2025
a6d5039
fix(constants): update excluded middleware operation name
AMoreaux Jan 31, 2025
15ea900
Merge remote-tracking branch 'origin/main' into feat/enable-custom-do…
AMoreaux Feb 3, 2025
19843e8
feat(domain-manager): remove unused hook useReadWorkspaceSubdomain
AMoreaux Feb 3, 2025
8de888a
refactor: update imports for utility and hook cleaning
AMoreaux Feb 3, 2025
277caeb
refactor(workspace): remove unused domain manager imports
AMoreaux Feb 3, 2025
190a61b
Merge branch 'main' into feat/enable-custom-domain-usage
AMoreaux Feb 3, 2025
0385ec4
feat(workspace): add custom domain entitlement validation
AMoreaux Feb 3, 2025
db308c9
Merge remote-tracking branch 'origin/feat/enable-custom-domain-usage'…
AMoreaux Feb 3, 2025
f84ef79
test(billing): add test cases for custom domain entitlement
AMoreaux Feb 3, 2025
5ba687d
Merge remote-tracking branch 'origin/main' into feat/enable-custom-do…
AMoreaux Feb 3, 2025
1b18290
Merge remote-tracking branch 'origin/main' into feat/enable-custom-do…
AMoreaux Feb 4, 2025
3953a3b
feat(graphql): add new types and queries for roles and environment va…
AMoreaux Feb 4, 2025
415a692
eslint
AMoreaux Feb 4, 2025
92f7d2b
Merge remote-tracking branch 'origin/main' into feat/enable-custom-do…
AMoreaux Feb 5, 2025
1a31374
feat(graphql): Replace subdomain field with workspaceUrls
AMoreaux Feb 5, 2025
ab16684
Merge remote-tracking branch 'origin/main' into feat/enable-custom-do…
AMoreaux Feb 6, 2025
5748bff
Merge remote-tracking branch 'origin/main' into feat/enable-custom-do…
AMoreaux Feb 6, 2025
d8b064f
refactor(auth): remove forceSubdomainUrl support
AMoreaux Feb 7, 2025
4ede2d3
test(domain-manager): remove useGetWorkspaceUrl test file
AMoreaux Feb 7, 2025
079cbe5
refactor(utils): centralize getWorkspaceUrl utility
AMoreaux Feb 7, 2025
aac4d9c
refactor(auth): remove unnecessary boolean argument
AMoreaux Feb 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down
2 changes: 1 addition & 1 deletion packages/twenty-front/codegen-metadata.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = {
},
config: {
namingConvention: { enumValues: 'keep' },
}
},
},
},
};
34 changes: 16 additions & 18 deletions packages/twenty-front/src/generated-metadata/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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'
}

Expand Down Expand Up @@ -706,7 +698,7 @@ export enum IdentityProviderType {
export type ImpersonateOutput = {
__typename?: 'ImpersonateOutput';
loginToken: AuthToken;
workspace: WorkspaceSubdomainAndId;
workspace: WorkspaceUrlsAndId;
};

export type Index = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1792,7 +1783,7 @@ export type SetupSsoOutput = {
export type SignUpOutput = {
__typename?: 'SignUpOutput';
loginToken: AuthToken;
workspace: WorkspaceSubdomainAndId;
workspace: WorkspaceUrlsAndId;
};

export enum SubscriptionInterval {
Expand Down Expand Up @@ -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']>;
AMoreaux marked this conversation as resolved.
Show resolved Hide resolved
deletedAt?: Maybe<Scalars['DateTime']['output']>;
disabled?: Maybe<Scalars['Boolean']['output']>;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 };
Expand Down
113 changes: 64 additions & 49 deletions packages/twenty-front/src/generated/graphql.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ export const SIGN_UP = gql`
}
workspace {
id
subdomain
workspaceUrls {
subdomainUrl
customUrl
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ export const CHECK_USER_EXISTS = gql`
availableWorkspaces {
id
displayName
subdomain
hostname
workspaceUrls {
subdomainUrl
customUrl
}
logo
sso {
type
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 8 additions & 7 deletions packages/twenty-front/src/modules/auth/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -96,8 +97,7 @@ export const useAuth = () => {
useGetLoginTokenFromEmailVerificationTokenMutation();
const [getCurrentUser] = useGetCurrentUserLazyQuery();

const { isOnAWorkspaceSubdomain } =
useIsCurrentLocationOnAWorkspaceSubdomain();
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();

const workspacePublicData = useRecoilValue(workspacePublicDataState);

Expand Down Expand Up @@ -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),
});
}

Expand All @@ -315,7 +315,7 @@ export const useAuth = () => {
};
}, [
getCurrentUser,
isOnAWorkspaceSubdomain,
isOnAWorkspace,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
Expand Down Expand Up @@ -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 && {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)};
Expand Down Expand Up @@ -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') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type CurrentWorkspace = Pick<
| 'hasValidEnterpriseKey'
| 'subdomain'
| 'hostname'
| 'workspaceUrls'
| 'metadataVersion'
>;

Expand Down
3 changes: 1 addition & 2 deletions packages/twenty-front/src/modules/auth/states/workspaces.ts
Original file line number Diff line number Diff line change
@@ -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>({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
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;
}

if (isDefined(searchParams)) {
Object.entries(searchParams).forEach(([key, value]) =>
url.searchParams.set(key, value),
url.searchParams.set(key, value.toString()),
);
}
return url.toString();
Expand Down
Loading
Loading