diff --git a/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx b/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx index 7d262d79293..d032c2b2898 100644 --- a/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx +++ b/apps/meteor/client/components/FingerprintChangeModalConfirmation.tsx @@ -8,12 +8,14 @@ import GenericModal from './GenericModal'; type FingerprintChangeModalConfirmationProps = { onConfirm: () => void; onCancel: () => void; + onClose: () => void; newWorkspace: boolean; }; const FingerprintChangeModalConfirmation = ({ onConfirm, onCancel, + onClose, newWorkspace, }: FingerprintChangeModalConfirmationProps): ReactElement => { const { t } = useTranslation(); @@ -25,6 +27,7 @@ const FingerprintChangeModalConfirmation = ({ onCancel={onCancel} cancelText={t('Back')} confirmText={newWorkspace ? t('Confirm_new_workspace') : t('Confirm_configuration_update')} + onClose={onClose} > { + switch (action.type) { + case 'openModal': + return { openModal: true, openConfirmation: false, newWorkspace: undefined }; + case 'openConfirmation': + return { openModal: false, openConfirmation: true, newWorkspace: action.newWorkspace }; + case 'closeModal': + return { openModal: false, openConfirmation: false, newWorkspace: undefined }; + default: + return state; + } +}; + +export const useFingerprintChange = () => { + const { t } = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + const isAdmin = useRole('admin'); + const setModal = useSetModal(); + const deploymentFingerPrintVerified = useSetting('Deployment_FingerPrint_Verified', true); + const fingerprintEndpoint = useEndpoint('POST', '/v1/fingerprint'); + + const [{ openConfirmation, openModal, newWorkspace }, dispatch] = useReducer(reducer, { + openModal: false, + openConfirmation: false, + newWorkspace: undefined, + }); + + const { mutate: fingerPrintMutation } = useMutation({ + mutationKey: ['settings', 'Deployment_FingerPrint_Verified'], + mutationFn: async (setDeploymentAs: 'new-workspace' | 'updated-configuration') => { + const result = await fingerprintEndpoint({ setDeploymentAs }); + return { + ...result, + setDeploymentAs, + }; + }, + onSuccess: ({ setDeploymentAs }) => { + if (setDeploymentAs === 'new-workspace') { + return dispatchToastMessage({ type: 'success', message: t('New_workspace_confirmed') }); + } + return dispatchToastMessage({ type: 'success', message: t('Configuration_update_confirmed') }); + }, + }); + + useEffect(() => { + if (!isAdmin) { + return; + } + if (deploymentFingerPrintVerified === null || deploymentFingerPrintVerified === true) { + return; + } + dispatch({ type: 'openModal' }); + + return () => { + dispatch({ type: 'closeModal' }); + }; + }, [deploymentFingerPrintVerified, isAdmin]); + + useEffect(() => { + if (!openModal && !openConfirmation) { + setModal(null); + } + if (openModal) { + setModal( + dispatch({ type: 'openConfirmation', newWorkspace: true })} + onCancel={() => dispatch({ type: 'openConfirmation', newWorkspace: false })} + onClose={() => dispatch({ type: 'closeModal' })} + />, + ); + } + + if (openConfirmation && newWorkspace !== undefined) { + setModal( + { + fingerPrintMutation(newWorkspace ? 'new-workspace' : 'updated-configuration'); + dispatch({ type: 'closeModal' }); + }} + onCancel={() => dispatch({ type: 'openModal' })} + onClose={() => dispatch({ type: 'closeModal' })} + newWorkspace={newWorkspace} + />, + ); + } + }, [fingerPrintMutation, setModal, openConfirmation, openModal, newWorkspace]); +}; diff --git a/apps/meteor/client/hooks/useRootUrlChange.tsx b/apps/meteor/client/hooks/useRootUrlChange.tsx new file mode 100644 index 00000000000..8180310fdad --- /dev/null +++ b/apps/meteor/client/hooks/useRootUrlChange.tsx @@ -0,0 +1,61 @@ +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useRole, useSetModal, useSetting, useSettingSetValue, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +import UrlChangeModal from '../components/UrlChangeModal'; + +export const useRootUrlChange = () => { + const { t } = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + const isAdmin = useRole('admin'); + const setModal = useSetModal(); + const closeModal = useEffectEvent(() => setModal(null)); + + const currentUrl = location.origin + window.__meteor_runtime_config__.ROOT_URL_PATH_PREFIX; + const siteUrl = useSetting('Site_Url', ''); + const documentDomain = useSetting('Document_Domain', ''); + const setSiteUrl = useSettingSetValue('Site_Url'); + + const { + mutate: siteUrlMutation, + isPending, + isSuccess, + } = useMutation({ + mutationKey: ['settings', 'Site_Url'], + mutationFn: async (url: string) => { + await setSiteUrl(url); + return { url }; + }, + onSuccess: ({ url }) => dispatchToastMessage({ type: 'success', message: t('Saved_new_url_site_is__url__', { url }) }), + onError: () => dispatchToastMessage({ type: 'error', message: t('Something_went_wrong') }), + }); + + useEffect(() => { + if (!isAdmin) { + return; + } + if (!siteUrl) { + return; + } + if (isPending || isSuccess) { + return; + } + if (window.__meteor_runtime_config__.ROOT_URL.replace(/\/$/, '') === currentUrl) { + return; + } + const onConfirm = () => { + closeModal(); + siteUrlMutation(currentUrl); + }; + + setModal(); + + if (documentDomain) { + window.document.domain = documentDomain; + } + + return closeModal; + }, [currentUrl, documentDomain, siteUrlMutation, siteUrl, isAdmin, isPending, isSuccess, setModal, closeModal]); +}; diff --git a/apps/meteor/client/hooks/useTwoFactorAuthSetupCheck.tsx b/apps/meteor/client/hooks/useTwoFactorAuthSetupCheck.tsx new file mode 100644 index 00000000000..47722794adc --- /dev/null +++ b/apps/meteor/client/hooks/useTwoFactorAuthSetupCheck.tsx @@ -0,0 +1,16 @@ +import { useSetModal } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { useRequire2faSetup } from '../views/hooks/useRequire2faSetup'; +import TwoFactorRequiredModal from '../views/root/MainLayout/TwoFactorRequiredModal'; + +export const useTwoFactorAuthSetupCheck = () => { + const setModal = useSetModal(); + const require2faSetup = useRequire2faSetup(); + + useEffect(() => { + if (require2faSetup) { + setModal(); + } + }, [setModal, require2faSetup]); +}; diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 061ba00111d..e013d5855d6 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -12,7 +12,6 @@ import './messageObserve'; import './messageTypes'; import './reloadRoomAfterLogin'; import './roles'; -import './rootUrlChange'; import './routes'; import './slashCommands'; import './startup'; diff --git a/apps/meteor/client/startup/rootUrlChange.ts b/apps/meteor/client/startup/rootUrlChange.ts deleted file mode 100644 index 4e42874eba4..00000000000 --- a/apps/meteor/client/startup/rootUrlChange.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { hasRole } from '../../app/authorization/client'; -import { Roles } from '../../app/models/client'; -import { settings } from '../../app/settings/client'; -import { sdk } from '../../app/utils/client/lib/SDKClient'; -import { t } from '../../app/utils/lib/i18n'; -import FingerprintChangeModal from '../components/FingerprintChangeModal'; -import FingerprintChangeModalConfirmation from '../components/FingerprintChangeModalConfirmation'; -import UrlChangeModal from '../components/UrlChangeModal'; -import { imperativeModal } from '../lib/imperativeModal'; -import { dispatchToastMessage } from '../lib/toast'; -import { isSyncReady } from '../lib/userData'; - -Meteor.startup(() => { - Tracker.autorun((c) => { - const userId = Meteor.userId(); - if (!userId) { - return; - } - - if (!Roles.ready.get() || !isSyncReady.get()) { - return; - } - - if (hasRole(userId, 'admin') === false) { - return c.stop(); - } - - const siteUrl = settings.get('Site_Url'); - if (!siteUrl) { - return; - } - - const currentUrl = location.origin + window.__meteor_runtime_config__.ROOT_URL_PATH_PREFIX; - if (window.__meteor_runtime_config__.ROOT_URL.replace(/\/$/, '') !== currentUrl) { - const confirm = (): void => { - imperativeModal.close(); - void sdk.call('saveSetting', 'Site_Url', currentUrl).then(() => { - dispatchToastMessage({ type: 'success', message: t('Saved') }); - }); - }; - imperativeModal.open({ - component: UrlChangeModal, - props: { - onConfirm: confirm, - siteUrl, - currentUrl, - onClose: imperativeModal.close, - }, - }); - } - - const documentDomain = settings.get('Document_Domain'); - if (documentDomain) { - window.document.domain = documentDomain; - } - - return c.stop(); - }); -}); - -Meteor.startup(() => { - Tracker.autorun((c) => { - const userId = Meteor.userId(); - if (!userId) { - return; - } - - if (!Roles.ready.get() || !isSyncReady.get()) { - return; - } - - if (hasRole(userId, 'admin') === false) { - return c.stop(); - } - - const deploymentFingerPrintVerified = settings.get('Deployment_FingerPrint_Verified'); - if (deploymentFingerPrintVerified == null || deploymentFingerPrintVerified === true) { - return; - } - - const updateWorkspace = (): void => { - imperativeModal.close(); - void sdk.rest.post('/v1/fingerprint', { setDeploymentAs: 'updated-configuration' }).then(() => { - dispatchToastMessage({ type: 'success', message: t('Configuration_update_confirmed') }); - }); - }; - - const setNewWorkspace = (): void => { - imperativeModal.close(); - void sdk.rest.post('/v1/fingerprint', { setDeploymentAs: 'new-workspace' }).then(() => { - dispatchToastMessage({ type: 'success', message: t('New_workspace_confirmed') }); - }); - }; - - const openModal = (): void => { - imperativeModal.open({ - component: FingerprintChangeModal, - props: { - onConfirm: () => { - imperativeModal.open({ - component: FingerprintChangeModalConfirmation, - props: { - onConfirm: setNewWorkspace, - onCancel: openModal, - newWorkspace: true, - }, - }); - }, - onCancel: () => { - imperativeModal.open({ - component: FingerprintChangeModalConfirmation, - props: { - onConfirm: updateWorkspace, - onCancel: openModal, - newWorkspace: false, - }, - }); - }, - onClose: imperativeModal.close, - }, - }); - }; - - openModal(); - - return c.stop(); - }); -}); diff --git a/apps/meteor/client/views/root/MainLayout/LoggedInArea.tsx b/apps/meteor/client/views/root/MainLayout/LoggedInArea.tsx index 16b64aa892d..d3e87235c54 100644 --- a/apps/meteor/client/views/root/MainLayout/LoggedInArea.tsx +++ b/apps/meteor/client/views/root/MainLayout/LoggedInArea.tsx @@ -4,7 +4,10 @@ import type { ReactNode } from 'react'; import { useCustomEmoji } from '../../../hooks/customEmoji/useCustomEmoji'; import { useNotificationUserCalendar } from '../../../hooks/notification/useNotificationUserCalendar'; import { useNotifyUser } from '../../../hooks/notification/useNotifyUser'; +import { useFingerprintChange } from '../../../hooks/useFingerprintChange'; import { useRestrictedRoles } from '../../../hooks/useRestrictedRoles'; +import { useRootUrlChange } from '../../../hooks/useRootUrlChange'; +import { useTwoFactorAuthSetupCheck } from '../../../hooks/useTwoFactorAuthSetupCheck'; import { useUnread } from '../../../hooks/useUnread'; import { useForceLogout } from '../hooks/useForceLogout'; import { useOTRMessaging } from '../hooks/useOTRMessaging'; @@ -29,6 +32,11 @@ const LoggedInArea = ({ children }: { children: ReactNode }) => { useStoreCookiesOnLogin(user._id); useCustomEmoji(); useRestrictedRoles(); + // These 3 hooks below need to be called in this order due to the way our `setModal` works. + // TODO: reevaluate `useSetModal` + useFingerprintChange(); + useRootUrlChange(); + useTwoFactorAuthSetupCheck(); return children; }; diff --git a/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx b/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx index 0b9bf5701f9..c0752f82091 100644 --- a/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx +++ b/apps/meteor/client/views/root/MainLayout/TwoFactorAuthSetupCheck.tsx @@ -1,11 +1,10 @@ import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; -import { useLayout, useSetModal } from '@rocket.chat/ui-contexts'; +import { useLayout } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; -import { lazy, useLayoutEffect } from 'react'; +import { lazy } from 'react'; import LayoutWithSidebar from './LayoutWithSidebar'; import LayoutWithSidebarV2 from './LayoutWithSidebarV2'; -import TwoFactorRequiredModal from './TwoFactorRequiredModal'; import { useRequire2faSetup } from '../../hooks/useRequire2faSetup'; const AccountSecurityPage = lazy(() => import('../../account/security/AccountSecurityPage')); @@ -13,13 +12,6 @@ const AccountSecurityPage = lazy(() => import('../../account/security/AccountSec const TwoFactorAuthSetupCheck = ({ children }: { children: ReactNode }): ReactElement => { const { isEmbedded: embeddedLayout } = useLayout(); const require2faSetup = useRequire2faSetup(); - const setModal = useSetModal(); - - useLayoutEffect(() => { - if (require2faSetup) { - setModal(); - } - }, [setModal, require2faSetup]); if (require2faSetup) { return ( diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 385c5173697..b58a6e987f3 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -4480,6 +4480,7 @@ "Save_your_encryption_password": "Save your encryption password", "Save_your_encryption_password_to_access": "Save your end-to-end encryption password to access", "Saved": "Saved", + "Saved_new_url_site_is__url__": "Saved, new url site is: {{url}}", "Saving": "Saving", "Scan_QR_code": "Using an authenticator app like Google Authenticator, Authy or Duo, scan the QR code. It will display a 6 digit code which you need to enter below.", "Scan_QR_code_alternative_s": "If you cannot scan the QR code, you may enter the following code manually into the authenticator app instead:",