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

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