review(front): refacto url-manager (#8861)

Replace https://github.com/twentyhq/twenty/pull/8855
This commit is contained in:
Antoine Moreaux 2024-12-05 11:47:51 +01:00 committed by GitHub
parent 7ab00a4c82
commit 081ecbcfaf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 639 additions and 271 deletions

View File

@ -7,5 +7,6 @@ GENERATE_SOURCEMAP=false
# VITE_DISABLE_TYPESCRIPT_CHECKER=true
# VITE_DISABLE_ESLINT_CHECKER=true
# VITE_ENABLE_SSL=false
# VITE_HOST=localhost.com
# SSL_KEY_PATH="./certs/your-cert.key"
# SSL_CERT_PATH="./certs/your-cert.crt"

View File

@ -14,6 +14,7 @@ import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/i
import { supportChatState } from '@/client-config/states/supportChatState';
import { email, mocks, password, results, token } from '../__mocks__/useAuth';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
const Wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks} addTypename={false}>
@ -83,6 +84,9 @@ describe('useAuth', () => {
);
const supportChat = useRecoilValue(supportChatState);
const isDebugMode = useRecoilValue(isDebugModeState);
const isMultiWorkspaceEnabled = useRecoilValue(
isMultiWorkspaceEnabledState,
);
return {
...useAuth(),
client,
@ -93,6 +97,7 @@ describe('useAuth', () => {
isDeveloperDefaultSignInPrefilled,
supportChat,
isDebugMode,
isMultiWorkspaceEnabled,
},
};
},

View File

@ -4,7 +4,6 @@ import {
snapshot_UNSTABLE,
useGotoRecoilSnapshot,
useRecoilCallback,
useRecoilValue,
useSetRecoilState,
} from 'recoil';
import { iconsState } from 'twenty-ui';
@ -42,18 +41,15 @@ import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDa
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
import { currentUserState } from '../states/currentUserState';
import { tokenPairState } from '../states/tokenPairState';
import { lastAuthenticateWorkspaceState } from '@/auth/states/lastAuthenticateWorkspaceState';
import { urlManagerState } from '@/url-manager/states/url-manager.state';
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain';
import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation';
export const useAuth = () => {
const setTokenPair = useSetRecoilState(tokenPairState);
const setCurrentUser = useSetRecoilState(currentUserState);
const urlManager = useRecoilValue(urlManagerState);
const setLastAuthenticateWorkspaceState = useSetRecoilState(
lastAuthenticateWorkspaceState,
);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
@ -68,7 +64,12 @@ export const useAuth = () => {
const [challenge] = useChallengeMutation();
const [signUp] = useSignUpMutation();
const [verify] = useVerifyMutation();
const { isTwentyWorkspaceSubdomain, getWorkspaceSubdomain } = useUrlManager();
const { isOnAWorkspaceSubdomain } =
useIsCurrentLocationOnAWorkspaceSubdomain();
const { workspaceSubdomain } = useReadWorkspaceSubdomainFromCurrentLocation();
const { setLastAuthenticateWorkspaceDomain } =
useLastAuthenticatedWorkspaceDomain();
const [checkUserExistsQuery, { data: checkUserExistsData }] =
useCheckUserExistsLazyQuery();
@ -101,6 +102,9 @@ export const useAuth = () => {
const isCurrentUserLoaded = snapshot
.getLoadable(isCurrentUserLoadedState)
.getValue();
const isMultiWorkspaceEnabled = snapshot
.getLoadable(isMultiWorkspaceEnabledState)
.getValue();
const initialSnapshot = emptySnapshot.map(({ set }) => {
set(iconsState, iconsValue);
set(authProvidersState, authProvidersValue);
@ -114,6 +118,7 @@ export const useAuth = () => {
set(captchaProviderState, captchaProvider);
set(clientConfigApiStatusState, clientConfigApiStatus);
set(isCurrentUserLoadedState, isCurrentUserLoaded);
set(isMultiWorkspaceEnabledState, isMultiWorkspaceEnabled);
return undefined;
});
goToRecoilSnapshot(initialSnapshot);
@ -212,13 +217,11 @@ export const useAuth = () => {
const workspace = user.defaultWorkspace ?? null;
setCurrentWorkspace(workspace);
if (isDefined(workspace) && isTwentyWorkspaceSubdomain) {
setLastAuthenticateWorkspaceState({
id: workspace.id,
if (isDefined(workspace) && isOnAWorkspaceSubdomain) {
setLastAuthenticateWorkspaceDomain({
workspaceId: workspace.id,
subdomain: workspace.subdomain,
cookieAttributes: {
domain: `.${urlManager.frontDomain}`,
},
});
}
@ -245,12 +248,11 @@ export const useAuth = () => {
setTokenPair,
setCurrentUser,
setCurrentWorkspace,
isTwentyWorkspaceSubdomain,
isOnAWorkspaceSubdomain,
setCurrentWorkspaceMembers,
setCurrentWorkspaceMember,
setDateTimeFormat,
setLastAuthenticateWorkspaceState,
urlManager.frontDomain,
setLastAuthenticateWorkspaceDomain,
setWorkspaces,
],
);
@ -340,15 +342,13 @@ export const useAuth = () => {
params.workspacePersonalInviteToken,
);
}
const subdomain = getWorkspaceSubdomain;
if (isDefined(subdomain)) {
url.searchParams.set('workspaceSubdomain', subdomain);
if (isDefined(workspaceSubdomain)) {
url.searchParams.set('workspaceSubdomain', workspaceSubdomain);
}
return url.toString();
},
[getWorkspaceSubdomain],
[workspaceSubdomain],
);
const handleGoogleLogin = useCallback(

View File

@ -30,8 +30,8 @@ import { useAuth } from '@/auth/hooks/useAuth';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { signInUpModeState } from '@/auth/states/signInUpModeState';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
import { SignInUpMode } from '@/auth/types/signInUpMode';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
const StyledContentContainer = styled(motion.div)`
margin-bottom: ${({ theme }) => theme.spacing(8)};
@ -53,8 +53,7 @@ export const SignInUpGlobalScopeForm = () => {
const { signInWithMicrosoft } = useSignInWithMicrosoft();
const { checkUserExists } = useAuth();
const { readCaptchaToken } = useReadCaptchaToken();
const { redirectToWorkspace } = useUrlManager();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
@ -97,7 +96,7 @@ export const SignInUpGlobalScopeForm = () => {
isDefined(data?.checkUserExists.availableWorkspaces) &&
data.checkUserExists.availableWorkspaces.length >= 1
) {
return redirectToWorkspace(
return redirectToWorkspaceDomain(
data?.checkUserExists.availableWorkspaces[0].subdomain,
pathname,
{

View File

@ -0,0 +1,72 @@
import { act, renderHook } from '@testing-library/react';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
// Mocks
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar');
jest.mock('~/generated/graphql');
describe('useHandleResetPassword', () => {
const enqueueSnackBarMock = jest.fn();
const emailPasswordResetLinkMock = jest.fn();
beforeEach(() => {
(useSnackBar as jest.Mock).mockReturnValue({
enqueueSnackBar: enqueueSnackBarMock,
});
(useEmailPasswordResetLinkMutation as jest.Mock).mockReturnValue([
emailPasswordResetLinkMock,
]);
jest.clearAllMocks();
});
it('should show error message if email is invalid', async () => {
const { result } = renderHook(() => useHandleResetPassword());
await act(() => result.current.handleResetPassword('')());
expect(enqueueSnackBarMock).toHaveBeenCalledWith('Invalid email', {
variant: SnackBarVariant.Error,
});
});
it('should show success message if password reset link is sent', async () => {
emailPasswordResetLinkMock.mockResolvedValue({
data: { emailPasswordResetLink: { success: true } },
});
const { result } = renderHook(() => useHandleResetPassword());
await act(() => result.current.handleResetPassword('test@example.com')());
expect(enqueueSnackBarMock).toHaveBeenCalledWith(
'Password reset link has been sent to the email',
{ variant: SnackBarVariant.Success },
);
});
it('should show error message if sending reset link fails', async () => {
emailPasswordResetLinkMock.mockResolvedValue({
data: { emailPasswordResetLink: { success: false } },
});
const { result } = renderHook(() => useHandleResetPassword());
await act(() => result.current.handleResetPassword('test@example.com')());
expect(enqueueSnackBarMock).toHaveBeenCalledWith('There was some issue', {
variant: SnackBarVariant.Error,
});
});
it('should show error message in case of request error', async () => {
const errorMessage = 'Network Error';
emailPasswordResetLinkMock.mockRejectedValue(new Error(errorMessage));
const { result } = renderHook(() => useHandleResetPassword());
await act(() => result.current.handleResetPassword('test@example.com')());
expect(enqueueSnackBarMock).toHaveBeenCalledWith(errorMessage, {
variant: SnackBarVariant.Error,
});
});
});

View File

@ -0,0 +1,73 @@
import { renderHook } from '@testing-library/react';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useGetAuthorizationUrlMutation } from '~/generated/graphql';
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
// Mock dependencies
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar');
jest.mock('~/generated/graphql');
// Helpers
const mockEnqueueSnackBar = jest.fn();
const mockGetAuthorizationUrlMutation = jest.fn();
// Mock return values
(useSnackBar as jest.Mock).mockReturnValue({
enqueueSnackBar: mockEnqueueSnackBar,
});
(useGetAuthorizationUrlMutation as jest.Mock).mockReturnValue([
mockGetAuthorizationUrlMutation,
]);
describe('useSSO', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should call getAuthorizationUrlForSSO with correct parameters', async () => {
const { result } = renderHook(() => useSSO());
const identityProviderId = 'test-id';
mockGetAuthorizationUrlMutation.mockResolvedValueOnce({
data: {
getAuthorizationUrl: {
authorizationURL: 'http://example.com',
},
},
});
await result.current.getAuthorizationUrlForSSO({ identityProviderId });
expect(mockGetAuthorizationUrlMutation).toHaveBeenCalledWith({
variables: { input: { identityProviderId } },
});
});
it('should enqueue error snackbar when URL retrieval fails', async () => {
const { result } = renderHook(() => useSSO());
const identityProviderId = 'test-id';
mockGetAuthorizationUrlMutation.mockResolvedValueOnce({
errors: [{ message: 'Error message' }],
});
await result.current.redirectToSSOLoginPage(identityProviderId);
expect(mockEnqueueSnackBar).toHaveBeenCalledWith('Error message', {
variant: 'error',
});
});
it('should enqueue default error snackbar when error message is not provided', async () => {
const { result } = renderHook(() => useSSO());
const identityProviderId = 'test-id';
mockGetAuthorizationUrlMutation.mockResolvedValueOnce({ errors: [{}] });
await result.current.redirectToSSOLoginPage(identityProviderId);
expect(mockEnqueueSnackBar).toHaveBeenCalledWith('Unknown error', {
variant: 'error',
});
});
});

View File

@ -0,0 +1,55 @@
import { renderHook } from '@testing-library/react';
import { useParams, useSearchParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth';
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
jest.mock('react-router-dom', () => ({
useParams: jest.fn(),
useSearchParams: jest.fn(),
}));
jest.mock('@/auth/hooks/useAuth', () => ({
useAuth: jest.fn(),
}));
describe('useSignInWithGoogle', () => {
it('should call signInWithGoogle with correct params', () => {
const signInWithGoogleMock = jest.fn();
const mockUseParams = { workspaceInviteHash: 'testHash' };
const mockSearchParams = new URLSearchParams('inviteToken=testToken');
(useParams as jest.Mock).mockReturnValue(mockUseParams);
(useSearchParams as jest.Mock).mockReturnValue([mockSearchParams]);
(useAuth as jest.Mock).mockReturnValue({
signInWithGoogle: signInWithGoogleMock,
});
const { result } = renderHook(() => useSignInWithGoogle());
result.current.signInWithGoogle();
expect(signInWithGoogleMock).toHaveBeenCalledWith({
workspaceInviteHash: 'testHash',
workspacePersonalInviteToken: 'testToken',
});
});
it('should call signInWithGoogle with undefined invite token if not present', () => {
const signInWithGoogleMock = jest.fn();
const mockUseParams = { workspaceInviteHash: 'testHash' };
const mockSearchParams = new URLSearchParams();
(useParams as jest.Mock).mockReturnValue(mockUseParams);
(useSearchParams as jest.Mock).mockReturnValue([mockSearchParams]);
(useAuth as jest.Mock).mockReturnValue({
signInWithGoogle: signInWithGoogleMock,
});
const { result } = renderHook(() => useSignInWithGoogle());
result.current.signInWithGoogle();
expect(signInWithGoogleMock).toHaveBeenCalledWith({
workspaceInviteHash: 'testHash',
workspacePersonalInviteToken: undefined,
});
});
});

View File

@ -0,0 +1,60 @@
import { renderHook } from '@testing-library/react';
import { useParams, useSearchParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth';
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
jest.mock('react-router-dom', () => ({
useParams: jest.fn(),
useSearchParams: jest.fn(),
}));
jest.mock('@/auth/hooks/useAuth', () => ({
useAuth: jest.fn(),
}));
describe('useSignInWithMicrosoft', () => {
it('should call signInWithMicrosoft with the correct parameters', () => {
const workspaceInviteHashMock = 'testHash';
const inviteTokenMock = 'testToken';
const signInWithMicrosoftMock = jest.fn();
(useParams as jest.Mock).mockReturnValue({
workspaceInviteHash: workspaceInviteHashMock,
});
(useSearchParams as jest.Mock).mockReturnValue([
new URLSearchParams(`inviteToken=${inviteTokenMock}`),
]);
(useAuth as jest.Mock).mockReturnValue({
signInWithMicrosoft: signInWithMicrosoftMock,
});
const { result } = renderHook(() => useSignInWithMicrosoft());
result.current.signInWithMicrosoft();
expect(signInWithMicrosoftMock).toHaveBeenCalledWith({
workspaceInviteHash: workspaceInviteHashMock,
workspacePersonalInviteToken: inviteTokenMock,
});
});
it('should handle missing inviteToken gracefully', () => {
const workspaceInviteHashMock = 'testHash';
const signInWithMicrosoftMock = jest.fn();
(useParams as jest.Mock).mockReturnValue({
workspaceInviteHash: workspaceInviteHashMock,
});
(useSearchParams as jest.Mock).mockReturnValue([new URLSearchParams('')]);
(useAuth as jest.Mock).mockReturnValue({
signInWithMicrosoft: signInWithMicrosoftMock,
});
const { result } = renderHook(() => useSignInWithMicrosoft());
result.current.signInWithMicrosoft();
expect(signInWithMicrosoftMock).toHaveBeenCalledWith({
workspaceInviteHash: workspaceInviteHashMock,
workspacePersonalInviteToken: undefined,
});
});
});

View File

@ -1,18 +0,0 @@
import { cookieStorageEffect } from '~/utils/recoil-effects';
import { Workspace } from '~/generated/graphql';
import { createState } from 'twenty-ui';
export const lastAuthenticateWorkspaceState = createState<
| (Pick<Workspace, 'id' | 'subdomain'> & {
cookieAttributes?: Cookies.CookieAttributes;
})
| null
>({
key: 'lastAuthenticateWorkspaceState',
defaultValue: null,
effects: [
cookieStorageEffect('lastAuthenticateWorkspace', {
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), // 1 year
}),
],
});

View File

@ -13,13 +13,13 @@ import { useEffect } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { useGetClientConfigQuery } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { urlManagerState } from '@/url-manager/states/url-manager.state';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { isSSOEnabledState } from '@/client-config/states/isSSOEnabledState';
export const ClientConfigProviderEffect = () => {
const setIsDebugMode = useSetRecoilState(isDebugModeState);
const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState);
const setUrlManager = useSetRecoilState(urlManagerState);
const setDomainConfiguration = useSetRecoilState(domainConfigurationState);
const setIsDeveloperDefaultSignInPrefilled = useSetRecoilState(
isDeveloperDefaultSignInPrefilledState,
@ -77,7 +77,6 @@ export const ClientConfigProviderEffect = () => {
setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
setIsDeveloperDefaultSignInPrefilled(data?.clientConfig.signInPrefilled);
setIsMultiWorkspaceEnabled(data?.clientConfig.isMultiWorkspaceEnabled);
setBilling(data?.clientConfig.billing);
setSupportChat(data?.clientConfig.support);
@ -95,7 +94,7 @@ export const ClientConfigProviderEffect = () => {
setChromeExtensionId(data?.clientConfig?.chromeExtensionId);
setApiConfig(data?.clientConfig?.api);
setIsSSOEnabledState(data?.clientConfig?.isSSOEnabled);
setUrlManager({
setDomainConfiguration({
defaultSubdomain: data?.clientConfig?.defaultSubdomain,
frontDomain: data?.clientConfig?.frontDomain,
});
@ -114,7 +113,7 @@ export const ClientConfigProviderEffect = () => {
setApiConfig,
setIsAnalyticsEnabled,
error,
setUrlManager,
setDomainConfiguration,
setIsSSOEnabledState,
]);

View File

@ -0,0 +1,34 @@
import { isDefined } from '~/utils/isDefined';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { useRecoilValue } from 'recoil';
export const useBuildWorkspaceUrl = () => {
const domainConfiguration = useRecoilValue(domainConfigurationState);
const buildWorkspaceUrl = (
subdomain?: string,
pathname?: string,
searchParams?: Record<string, string>,
) => {
const url = new URL(window.location.href);
if (isDefined(subdomain) && subdomain.length !== 0) {
url.hostname = `${subdomain}.${domainConfiguration.frontDomain}`;
}
if (isDefined(pathname)) {
url.pathname = pathname;
}
if (isDefined(searchParams)) {
Object.entries(searchParams).forEach(([key, value]) =>
url.searchParams.set(key, value),
);
}
return url.toString();
};
return {
buildWorkspaceUrl,
};
};

View File

@ -0,0 +1,42 @@
import { useGetPublicWorkspaceDataBySubdomainQuery } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRedirectToDefaultDomain } from '@/domain-manager/hooks/useRedirectToDefaultDomain';
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain';
export const useGetPublicWorkspaceDataBySubdomain = () => {
const { isDefaultDomain } = useIsCurrentLocationOnDefaultDomain();
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const setAuthProviders = useSetRecoilState(authProvidersState);
const workspacePublicData = useRecoilValue(workspacePublicDataState);
const { redirectToDefaultDomain } = useRedirectToDefaultDomain();
const setWorkspacePublicDataState = useSetRecoilState(
workspacePublicDataState,
);
const { setLastAuthenticateWorkspaceDomain } =
useLastAuthenticatedWorkspaceDomain();
const { loading } = useGetPublicWorkspaceDataBySubdomainQuery({
skip:
(isMultiWorkspaceEnabled && isDefaultDomain) ||
isDefined(workspacePublicData),
onCompleted: (data) => {
setAuthProviders(data.getPublicWorkspaceDataBySubdomain.authProviders);
setWorkspacePublicDataState(data.getPublicWorkspaceDataBySubdomain);
},
onError: (error) => {
// eslint-disable-next-line no-console
console.error(error);
setLastAuthenticateWorkspaceDomain(null);
redirectToDefaultDomain();
},
});
return {
loading,
};
};

View File

@ -0,0 +1,27 @@
import { isDefined } from '~/utils/isDefined';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useRecoilValue } from 'recoil';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { useReadDefaultDomainFromConfiguration } from '@/domain-manager/hooks/useReadDefaultDomainFromConfiguration';
export const useIsCurrentLocationOnAWorkspaceSubdomain = () => {
const { defaultDomain } = useReadDefaultDomainFromConfiguration();
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const domainConfiguration = useRecoilValue(domainConfigurationState);
if (
isMultiWorkspaceEnabled &&
(!isDefined(domainConfiguration.frontDomain) ||
!isDefined(domainConfiguration.defaultSubdomain))
) {
throw new Error('frontDomain and defaultSubdomain are required');
}
const isOnAWorkspaceSubdomain =
isMultiWorkspaceEnabled && window.location.hostname !== defaultDomain;
return {
isOnAWorkspaceSubdomain,
};
};

View File

@ -0,0 +1,15 @@
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useRecoilValue } from 'recoil';
import { useReadDefaultDomainFromConfiguration } from '@/domain-manager/hooks/useReadDefaultDomainFromConfiguration';
export const useIsCurrentLocationOnDefaultDomain = () => {
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const { defaultDomain } = useReadDefaultDomainFromConfiguration();
const isDefaultDomain = isMultiWorkspaceEnabled
? window.location.hostname === defaultDomain
: true;
return {
isDefaultDomain,
};
};

View File

@ -0,0 +1,29 @@
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { lastAuthenticatedWorkspaceDomainState } from '@/domain-manager/states/lastAuthenticatedWorkspaceDomainState';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
export const useLastAuthenticatedWorkspaceDomain = () => {
const domainConfiguration = useRecoilValue(domainConfigurationState);
const setLastAuthenticatedWorkspaceDomain = useSetRecoilState(
lastAuthenticatedWorkspaceDomainState,
);
const setLastAuthenticateWorkspaceDomainWithCookieAttributes = (
params: { workspaceId: string; subdomain: string } | null,
) => {
setLastAuthenticatedWorkspaceDomain(
params
? {
...params,
cookieAttributes: {
domain: `.${domainConfiguration.frontDomain}`,
},
}
: null,
);
};
return {
setLastAuthenticateWorkspaceDomain:
setLastAuthenticateWorkspaceDomainWithCookieAttributes,
};
};

View File

@ -0,0 +1,16 @@
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useRecoilValue } from 'recoil';
export const useReadDefaultDomainFromConfiguration = () => {
const domainConfiguration = useRecoilValue(domainConfigurationState);
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const defaultDomain = isMultiWorkspaceEnabled
? `${domainConfiguration.defaultSubdomain}.${domainConfiguration.frontDomain}`
: domainConfiguration.frontDomain;
return {
defaultDomain,
};
};

View File

@ -0,0 +1,24 @@
import { isDefined } from '~/utils/isDefined';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { useRecoilValue } from 'recoil';
import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain';
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,16 @@
import { useReadDefaultDomainFromConfiguration } from '@/domain-manager/hooks/useReadDefaultDomainFromConfiguration';
export const useRedirectToDefaultDomain = () => {
const { defaultDomain } = useReadDefaultDomainFromConfiguration();
const redirectToDefaultDomain = () => {
const url = new URL(window.location.href);
if (url.hostname !== defaultDomain) {
url.hostname = defaultDomain;
window.location.href = url.toString();
}
};
return {
redirectToDefaultDomain,
};
};

View File

@ -0,0 +1,21 @@
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useRecoilValue } from 'recoil';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
export const useRedirectToWorkspaceDomain = () => {
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
const redirectToWorkspaceDomain = (
subdomain: string,
pathname?: string,
searchParams?: Record<string, string>,
) => {
if (!isMultiWorkspaceEnabled) return;
window.location.href = buildWorkspaceUrl(subdomain, pathname, searchParams);
};
return {
redirectToWorkspaceDomain,
};
};

View File

@ -1,10 +1,10 @@
import { createState } from 'twenty-ui';
import { ClientConfig } from '~/generated/graphql';
export const urlManagerState = createState<
export const domainConfigurationState = createState<
Pick<ClientConfig, 'frontDomain' | 'defaultSubdomain'>
>({
key: 'urlManager',
key: 'domainConfiguration',
defaultValue: {
frontDomain: '',
defaultSubdomain: undefined,

View File

@ -0,0 +1,16 @@
import { cookieStorageEffect } from '~/utils/recoil-effects';
import { createState } from 'twenty-ui';
export const lastAuthenticatedWorkspaceDomainState = createState<{
subdomain: string;
workspaceId: string;
cookieAttributes?: Cookies.CookieAttributes;
} | null>({
key: 'lastAuthenticateWorkspaceDomain',
defaultValue: null,
effects: [
cookieStorageEffect('lastAuthenticateWorkspaceDomain', {
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), // 1 year
}),
],
});

View File

@ -61,7 +61,8 @@ export const SettingsSecurityOptionsList = () => {
if (
currentWorkspace[key] === true &&
allAuthProvidersEnabled.filter((isAuthEnable) => isAuthEnable).length <= 1
allAuthProvidersEnabled.filter((isAuthEnabled) => isAuthEnabled).length <=
1
) {
return enqueueSnackBar(
'At least one authentication method must be enabled',

View File

@ -13,15 +13,13 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { IconChevronDown, MenuItemSelectAvatar } from 'twenty-ui';
import {
IconChevronDown,
MenuItemSelectAvatar,
UndecoratedLink,
} from 'twenty-ui';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
import { Link } from 'react-router-dom';
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
const StyledLink = styled(Link)`
text-decoration: none;
width: 100%;
`;
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
const StyledLogo = styled.div<{ logo: string }>`
background: url(${({ logo }) => logo});
@ -79,7 +77,7 @@ export const MultiWorkspaceDropdownButton = ({
useState(false);
const { switchWorkspace } = useWorkspaceSwitching();
const { buildWorkspaceUrl } = useUrlManager();
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
const { closeDropdown } = useDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
@ -122,9 +120,13 @@ export const MultiWorkspaceDropdownButton = ({
dropdownComponents={
<DropdownMenuItemsContainer>
{workspaces.map((workspace) => (
<StyledLink
<UndecoratedLink
key={workspace.id}
to={buildWorkspaceUrl(workspace.subdomain)}
onClick={(event) => {
event?.preventDefault();
handleChange(workspace.id);
}}
>
<MenuItemSelectAvatar
text={workspace.displayName ?? ''}
@ -136,12 +138,8 @@ export const MultiWorkspaceDropdownButton = ({
/>
}
selected={currentWorkspace?.id === workspace.id}
onClick={(event) => {
event?.preventDefault();
handleChange(workspace.id);
}}
/>
</StyledLink>
</UndecoratedLink>
))}
</DropdownMenuItemsContainer>
}

View File

@ -7,14 +7,16 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSwitchWorkspaceMutation } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
import { useRedirectToDefaultDomain } from '@/domain-manager/hooks/useRedirectToDefaultDomain';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
export const useWorkspaceSwitching = () => {
const [switchWorkspaceMutation] = useSwitchWorkspaceMutation();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const { enqueueSnackBar } = useSnackBar();
const { redirectToHome, redirectToWorkspace } = useUrlManager();
const { redirectToDefaultDomain } = useRedirectToDefaultDomain();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const switchWorkspace = async (workspaceId: string) => {
if (currentWorkspace?.id === workspaceId) return;
@ -35,10 +37,10 @@ export const useWorkspaceSwitching = () => {
});
if (isDefined(errors) || !isDefined(data?.switchWorkspace.subdomain)) {
return redirectToHome();
return redirectToDefaultDomain();
}
redirectToWorkspace(data.switchWorkspace.subdomain);
redirectToWorkspaceDomain(data.switchWorkspace.subdomain);
};
return { switchWorkspace };

View File

@ -1,110 +0,0 @@
import { useMemo, useCallback } from 'react';
import { isDefined } from '~/utils/isDefined';
import { urlManagerState } from '@/url-manager/states/url-manager.state';
import { useRecoilValue } from 'recoil';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
export const useUrlManager = () => {
const urlManager = useRecoilValue(urlManagerState);
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const homePageDomain = useMemo(() => {
return isMultiWorkspaceEnabled
? `${urlManager.defaultSubdomain}.${urlManager.frontDomain}`
: urlManager.frontDomain;
}, [
isMultiWorkspaceEnabled,
urlManager.defaultSubdomain,
urlManager.frontDomain,
]);
const isTwentyHomePage = useMemo(() => {
if (!isMultiWorkspaceEnabled) return true;
return window.location.hostname === homePageDomain;
}, [homePageDomain, isMultiWorkspaceEnabled]);
const isTwentyWorkspaceSubdomain = useMemo(() => {
if (!isMultiWorkspaceEnabled) return false;
if (
!isDefined(urlManager.frontDomain) ||
!isDefined(urlManager.defaultSubdomain)
) {
throw new Error('frontDomain and defaultSubdomain are required');
}
return window.location.hostname !== homePageDomain;
}, [
homePageDomain,
isMultiWorkspaceEnabled,
urlManager.defaultSubdomain,
urlManager.frontDomain,
]);
const getWorkspaceSubdomain = useMemo(() => {
if (!isDefined(urlManager.frontDomain)) {
throw new Error('frontDomain is not defined');
}
return isTwentyWorkspaceSubdomain
? window.location.hostname.replace(`.${urlManager.frontDomain}`, '')
: null;
}, [isTwentyWorkspaceSubdomain, urlManager.frontDomain]);
const buildWorkspaceUrl = useCallback(
(
subdomain?: string,
onPage?: string,
searchParams?: Record<string, string>,
) => {
const url = new URL(window.location.href);
if (isDefined(subdomain) && subdomain.length !== 0) {
url.hostname = `${subdomain}.${urlManager.frontDomain}`;
}
if (isDefined(onPage)) {
url.pathname = onPage;
}
if (isDefined(searchParams)) {
Object.entries(searchParams).forEach(([key, value]) =>
url.searchParams.set(key, value),
);
}
return url.toString();
},
[urlManager.frontDomain],
);
const redirectToWorkspace = useCallback(
(
subdomain: string,
onPage?: string,
searchParams?: Record<string, string>,
) => {
if (!isMultiWorkspaceEnabled) return;
window.location.href = buildWorkspaceUrl(subdomain, onPage, searchParams);
},
[buildWorkspaceUrl, isMultiWorkspaceEnabled],
);
const redirectToHome = useCallback(() => {
const url = new URL(window.location.href);
if (url.hostname !== homePageDomain) {
url.hostname = homePageDomain;
window.location.href = url.toString();
}
}, [homePageDomain]);
return {
redirectToHome,
redirectToWorkspace,
homePageDomain,
isTwentyHomePage,
buildWorkspaceUrl,
isTwentyWorkspaceSubdomain,
getWorkspaceSubdomain,
};
};

View File

@ -1,79 +1,52 @@
import { useRecoilValue, useSetRecoilState, useRecoilState } from 'recoil';
import { useGetPublicWorkspaceDataBySubdomainQuery } from '~/generated/graphql';
import { useRecoilValue } from 'recoil';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { useEffect } from 'react';
import { isDefined } from '~/utils/isDefined';
import { lastAuthenticateWorkspaceState } from '@/auth/states/lastAuthenticateWorkspaceState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { lastAuthenticatedWorkspaceDomainState } from '@/domain-manager/states/lastAuthenticatedWorkspaceDomainState';
import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation';
import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain';
export const WorkspaceProviderEffect = () => {
const workspacePublicData = useRecoilValue(workspacePublicDataState);
const setAuthProviders = useSetRecoilState(authProvidersState);
const setWorkspacePublicDataState = useSetRecoilState(
workspacePublicDataState,
const lastAuthenticatedWorkspaceDomain = useRecoilValue(
lastAuthenticatedWorkspaceDomainState,
);
const [lastAuthenticateWorkspace, setLastAuthenticateWorkspace] =
useRecoilState(lastAuthenticateWorkspaceState);
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const { isDefaultDomain } = useIsCurrentLocationOnDefaultDomain();
const {
redirectToHome,
getWorkspaceSubdomain,
redirectToWorkspace,
isTwentyHomePage,
} = useUrlManager();
const { workspaceSubdomain } = useReadWorkspaceSubdomainFromCurrentLocation();
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
useGetPublicWorkspaceDataBySubdomainQuery({
skip:
(isMultiWorkspaceEnabled && isTwentyHomePage) ||
isDefined(workspacePublicData),
onCompleted: (data) => {
setAuthProviders(data.getPublicWorkspaceDataBySubdomain.authProviders);
setWorkspacePublicDataState(data.getPublicWorkspaceDataBySubdomain);
},
onError: (error) => {
// eslint-disable-next-line no-console
console.error(error);
setLastAuthenticateWorkspace(null);
redirectToHome();
},
});
useEffect(() => {
if (
isMultiWorkspaceEnabled &&
isDefined(workspacePublicData?.subdomain) &&
workspacePublicData.subdomain !== getWorkspaceSubdomain
) {
redirectToWorkspace(workspacePublicData.subdomain);
if (isMultiWorkspaceEnabled && isDefined(workspacePublicData?.subdomain)) {
redirectToWorkspaceDomain(workspacePublicData.subdomain);
}
}, [
getWorkspaceSubdomain,
workspaceSubdomain,
isMultiWorkspaceEnabled,
redirectToWorkspace,
redirectToWorkspaceDomain,
workspacePublicData,
]);
useEffect(() => {
if (
isMultiWorkspaceEnabled &&
isDefined(lastAuthenticateWorkspace?.subdomain) &&
isTwentyHomePage
isDefined(lastAuthenticatedWorkspaceDomain?.subdomain) &&
isDefaultDomain
) {
redirectToWorkspace(lastAuthenticateWorkspace.subdomain);
redirectToWorkspaceDomain(lastAuthenticatedWorkspaceDomain.subdomain);
}
}, [
isMultiWorkspaceEnabled,
isTwentyHomePage,
lastAuthenticateWorkspace,
redirectToWorkspace,
isDefaultDomain,
lastAuthenticatedWorkspaceDomain,
redirectToWorkspaceDomain,
]);
return <></>;

View File

@ -14,27 +14,33 @@ import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInU
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/SignInUpSSOIdentityProviderSelection';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
import { useMemo } from 'react';
import { isDefined } from '~/utils/isDefined';
import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeFormEffect';
import { useGetPublicWorkspaceDataBySubdomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain';
import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain';
import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain';
export const SignInUp = () => {
const { form } = useSignInUpForm();
const { signInUpStep } = useSignInUp(form);
const { isTwentyHomePage, isTwentyWorkspaceSubdomain } = useUrlManager();
const { isDefaultDomain } = useIsCurrentLocationOnDefaultDomain();
const { isOnAWorkspaceSubdomain } =
useIsCurrentLocationOnAWorkspaceSubdomain();
const workspacePublicData = useRecoilValue(workspacePublicDataState);
const { loading } = useGetPublicWorkspaceDataBySubdomain();
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const signInUpForm = useMemo(() => {
if (isTwentyHomePage && isMultiWorkspaceEnabled) {
if (loading) return null;
if (isDefaultDomain && isMultiWorkspaceEnabled) {
return <SignInUpGlobalScopeForm />;
}
if (
(!isMultiWorkspaceEnabled ||
(isMultiWorkspaceEnabled && isTwentyWorkspaceSubdomain)) &&
(isMultiWorkspaceEnabled && isOnAWorkspaceSubdomain)) &&
signInUpStep === SignInUpStep.SSOIdentityProviderSelection
) {
return <SignInUpSSOIdentityProviderSelection />;
@ -42,7 +48,7 @@ export const SignInUp = () => {
if (
isDefined(workspacePublicData) &&
(!isMultiWorkspaceEnabled || isTwentyWorkspaceSubdomain)
(!isMultiWorkspaceEnabled || isOnAWorkspaceSubdomain)
) {
return (
<>
@ -54,9 +60,10 @@ export const SignInUp = () => {
return <SignInUpGlobalScopeForm />;
}, [
isTwentyHomePage,
isDefaultDomain,
isMultiWorkspaceEnabled,
isTwentyWorkspaceSubdomain,
isOnAWorkspaceSubdomain,
loading,
signInUpStep,
workspacePublicData,
]);

View File

@ -24,7 +24,7 @@ import {
import { isDefined } from '~/utils/isDefined';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { AppPath } from '@/types/AppPath';
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
const StyledContentContainer = styled.div`
width: 100%;
@ -51,7 +51,7 @@ export const CreateWorkspace = () => {
const { enqueueSnackBar } = useSnackBar();
const onboardingStatus = useOnboardingStatus();
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const { redirectToWorkspace } = useUrlManager();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const [activateWorkspace] = useActivateWorkspaceMutation();
const apolloMetadataClient = useApolloMetadataClient();
@ -84,7 +84,7 @@ export const CreateWorkspace = () => {
setIsCurrentUserLoaded(false);
if (isDefined(result.data) && isMultiWorkspaceEnabled) {
return redirectToWorkspace(
return redirectToWorkspaceDomain(
result.data.activateWorkspace.workspace.subdomain,
AppPath.Verify,
{
@ -111,7 +111,7 @@ export const CreateWorkspace = () => {
setIsCurrentUserLoaded,
isMultiWorkspaceEnabled,
apolloMetadataClient,
redirectToWorkspace,
redirectToWorkspaceDomain,
enqueueSnackBar,
],
);

View File

@ -1,9 +1,12 @@
import { GithubVersionLink, H2Title, Section, IconWorld } from 'twenty-ui';
import { Link } from 'react-router-dom';
import {
GithubVersionLink,
H2Title,
Section,
IconWorld,
UndecoratedLink,
} from 'twenty-ui';
import { useRecoilValue } from 'recoil';
import styled from '@emotion/styled';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { DeleteWorkspace } from '@/settings/profile/components/DeleteWorkspace';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
@ -16,10 +19,6 @@ import packageJson from '../../../package.json';
import { SettingsCard } from '@/settings/components/SettingsCard';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
const StyledLink = styled(Link)`
text-decoration: none;
`;
export const SettingsWorkspace = () => {
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
return (
@ -48,9 +47,9 @@ export const SettingsWorkspace = () => {
title="Domain"
description="Edit your subdomain name or set a custom domain."
/>
<StyledLink to={getSettingsPagePath(SettingsPath.Domain)}>
<UndecoratedLink to={getSettingsPagePath(SettingsPath.Domain)}>
<SettingsCard title="Customize Domain" Icon={<IconWorld />} />
</StyledLink>
</UndecoratedLink>
</Section>
)}

View File

@ -15,9 +15,9 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac
import { useNavigate } from 'react-router-dom';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useUpdateWorkspaceMutation } from '~/generated/graphql';
import { useUrlManager } from '@/url-manager/hooks/useUrlManager';
import { urlManagerState } from '@/url-manager/states/url-manager.state';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { isDefined } from '~/utils/isDefined';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
const validationSchema = z
.object({
@ -39,17 +39,17 @@ const StyledDomain = styled.h2`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: 8px;
margin-left: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsDomain = () => {
const navigate = useNavigate();
const urlManager = useRecoilValue(urlManagerState);
const domainConfiguration = useRecoilValue(domainConfigurationState);
const { enqueueSnackBar } = useSnackBar();
const [updateWorkspace] = useUpdateWorkspaceMutation();
const { buildWorkspaceUrl } = useUrlManager();
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
@ -142,8 +142,8 @@ export const SettingsDomain = () => {
/>
)}
/>
{isDefined(urlManager) && isDefined(urlManager.frontDomain) && (
<StyledDomain>.{urlManager.frontDomain}</StyledDomain>
{isDefined(domainConfiguration.frontDomain) && (
<StyledDomain>.{domainConfiguration.frontDomain}</StyledDomain>
)}
</StyledDomainFromWrapper>
)}

View File

@ -1,5 +1,6 @@
import { AtomEffect } from 'recoil';
import omit from 'lodash.omit';
import { z } from 'zod';
import { cookieStorage } from '~/utils/cookie-storage';
@ -20,6 +21,20 @@ export const localStorageEffect =
});
};
const customCookieAttributeZodSchema = z.object({
cookieAttributes: z.object({
expires: z.union([z.number(), z.instanceof(Date)]).optional(),
path: z.string().optional(),
domain: z.string().optional(),
secure: z.boolean().optional(),
}),
});
export const isCustomCookiesAttributesValue = (
value: unknown,
): value is { cookieAttributes: Cookies.CookieAttributes } =>
customCookieAttributeZodSchema.safeParse(value).success;
export const cookieStorageEffect =
<T>(
key: string,
@ -52,9 +67,7 @@ export const cookieStorageEffect =
const cookieAttributes = {
...defaultAttributes,
...(typeof newValue === 'object' &&
'cookieAttributes' in newValue &&
typeof newValue.cookieAttributes === 'object'
...(isCustomCookiesAttributesValue(newValue)
? newValue.cookieAttributes
: {}),
};

View File

@ -19,7 +19,9 @@ export default defineConfig(({ command, mode }) => {
VITE_BUILD_SOURCEMAP,
VITE_DISABLE_TYPESCRIPT_CHECKER,
VITE_DISABLE_ESLINT_CHECKER,
VITE_ENABLE_SSL,
VITE_HOST,
SSL_CERT_PATH,
SSL_KEY_PATH,
REACT_APP_PORT,
} = env;
@ -64,27 +66,24 @@ export default defineConfig(({ command, mode }) => {
};
}
if (VITE_ENABLE_SSL && (!env.SSL_KEY_PATH || !env.SSL_CERT_PATH)) {
throw new Error(
'to use https SSL_KEY_PATH and SSL_CERT_PATH must be both defined',
);
}
return {
root: __dirname,
cacheDir: '../../node_modules/.vite/packages/twenty-front',
server: {
port: port,
protocol: VITE_ENABLE_SSL ? 'https' : 'http',
...(VITE_ENABLE_SSL
...(VITE_HOST ? { host: VITE_HOST } : {}),
...(SSL_KEY_PATH && SSL_CERT_PATH
? {
protocol: 'https',
https: {
key: fs.readFileSync(env.SSL_KEY_PATH),
cert: fs.readFileSync(env.SSL_CERT_PATH),
},
}
: {}),
: {
protocol: 'http',
}),
fs: {
allow: [
searchForWorkspaceRoot(process.cwd()),