mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 03:51:36 +03:00
review(front): refacto url-manager (#8861)
Replace https://github.com/twentyhq/twenty/pull/8855
This commit is contained in:
parent
7ab00a4c82
commit
081ecbcfaf
@ -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"
|
@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
{
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
@ -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
|
||||
}),
|
||||
],
|
||||
});
|
@ -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,
|
||||
]);
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
@ -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
|
||||
}),
|
||||
],
|
||||
});
|
@ -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',
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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 };
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
@ -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 <></>;
|
||||
|
@ -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,
|
||||
]);
|
||||
|
@ -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,
|
||||
],
|
||||
);
|
||||
|
@ -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>
|
||||
)}
|
||||
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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
|
||||
: {}),
|
||||
};
|
||||
|
@ -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()),
|
||||
|
Loading…
Reference in New Issue
Block a user