diff --git a/apps/meteor/app/api/server/v1/cloud.ts b/apps/meteor/app/api/server/v1/cloud.ts index d7e108a7b034..38b6a7221871 100644 --- a/apps/meteor/app/api/server/v1/cloud.ts +++ b/apps/meteor/app/api/server/v1/cloud.ts @@ -5,6 +5,7 @@ import { hasPermission, hasRole } from '../../../authorization/server'; import { saveRegistrationData } from '../../../cloud/server/functions/saveRegistrationData'; import { retrieveRegistrationStatus } from '../../../cloud/server/functions/retrieveRegistrationStatus'; import { startRegisterWorkspaceSetupWizard } from '../../../cloud/server/functions/startRegisterWorkspaceSetupWizard'; +import { registerPreIntentWorkspaceWizard } from '../../../cloud/server/functions/registerPreIntentWorkspaceWizard'; import { getConfirmationPoll } from '../../../cloud/server/functions/getConfirmationPoll'; API.v1.addRoute( @@ -60,6 +61,20 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'cloud.registerPreIntent', + { authRequired: true }, + { + async post() { + if (!hasPermission(this.userId, 'manage-cloud')) { + return API.v1.unauthorized(); + } + + return API.v1.success({ offline: !(await registerPreIntentWorkspaceWizard()) }); + }, + }, +); + API.v1.addRoute( 'cloud.confirmationPoll', { authRequired: true }, diff --git a/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts b/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts new file mode 100644 index 000000000000..c4f12aeaa9fe --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts @@ -0,0 +1,33 @@ +import { HTTP } from 'meteor/http'; +import type { IUser } from '@rocket.chat/core-typings'; + +import { settings } from '../../../settings/server'; +import { buildWorkspaceRegistrationData } from './buildRegistrationData'; +import { SystemLogger } from '../../../../server/lib/logger/system'; +import { Users } from '../../../models/server'; + +export async function registerPreIntentWorkspaceWizard(): Promise { + const firstUser = Users.getOldest({ name: 1, emails: 1 }) as IUser | undefined; + const email = firstUser?.emails?.find((address) => address)?.address || ''; + + const regInfo = await buildWorkspaceRegistrationData(email); + const cloudUrl = settings.get('Cloud_Url'); + + try { + HTTP.post(`${cloudUrl}/api/v2/register/workspace/pre-intent`, { + data: regInfo, + timeout: 10 * 1000, + }); + + return true; + } catch (err: any) { + SystemLogger.error({ + msg: 'Failed to register workspace pre-intent with Rocket.Chat Cloud', + url: '/api/v2/register/workspace/pre-intent', + ...(err.response?.data && { cloudError: err.response.data }), + err, + }); + + return false; + } +} diff --git a/apps/meteor/client/views/admin/cloud/CloudPage.tsx b/apps/meteor/client/views/admin/cloud/CloudPage.tsx index 4dfb19ed0317..9ffdf98e4aae 100644 --- a/apps/meteor/client/views/admin/cloud/CloudPage.tsx +++ b/apps/meteor/client/views/admin/cloud/CloudPage.tsx @@ -11,7 +11,7 @@ import { } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactNode } from 'react'; -import React, { useEffect } from 'react'; +import React, { useEffect, useCallback } from 'react'; import Page from '../../../components/Page'; import ConnectToCloudSection from './ConnectToCloudSection'; @@ -28,6 +28,7 @@ const CloudPage = function CloudPage(): ReactNode { const cloudRoute = useRoute('cloud'); + const shouldOpenManualRegistration = useQueryStringParameter('register'); const page = useRouteParameter('page'); const errorCode = useQueryStringParameter('error_code'); @@ -95,13 +96,19 @@ const CloudPage = function CloudPage(): ReactNode { acceptWorkspaceToken(); }, [reload, connectWorkspace, dispatchToastMessage, t, token]); - const handleManualWorkspaceRegistrationButtonClick = (): void => { + const handleManualWorkspaceRegistrationButtonClick = useCallback((): void => { const handleModalClose = (): void => { setModal(null); reload(); }; setModal(); - }; + }, [setModal, reload]); + + useEffect(() => { + if (shouldOpenManualRegistration) { + handleManualWorkspaceRegistrationButtonClick(); + } + }, [shouldOpenManualRegistration, handleManualWorkspaceRegistrationButtonClick]); if (result.isLoading || result.isError) { return null; diff --git a/apps/meteor/client/views/setupWizard/contexts/SetupWizardContext.tsx b/apps/meteor/client/views/setupWizard/contexts/SetupWizardContext.tsx index 21cae1fade1e..b6c09fb4a7d7 100644 --- a/apps/meteor/client/views/setupWizard/contexts/SetupWizardContext.tsx +++ b/apps/meteor/client/views/setupWizard/contexts/SetupWizardContext.tsx @@ -1,12 +1,11 @@ import type { ISetting } from '@rocket.chat/core-typings'; -import type { AdminInfoPage, OrganizationInfoPage, RegisteredServerPage } from '@rocket.chat/onboarding-ui'; +import type { AdminInfoPage, OrganizationInfoPage, RegisterServerPage } from '@rocket.chat/onboarding-ui'; import type { ComponentProps, Dispatch, SetStateAction } from 'react'; import { createContext, useContext } from 'react'; type SetupWizardData = { - adminData: Omit['onSubmit']>[0], 'keepPosted'>; organizationData: Parameters['onSubmit']>[0]; - serverData: Parameters['onSubmit']>[0]; + serverData: Parameters['onSubmit']>[0]; registrationData: { device_code: string; user_code: string; @@ -28,17 +27,18 @@ type SetupWizarContextValue = { goToPreviousStep: () => void; goToNextStep: () => void; goToStep: (step: number) => void; - registerAdminUser: () => Promise; + registerAdminUser: (user: Omit['onSubmit']>[0], 'keepPosted'>) => Promise; registerServer: (params: { email: string; resend?: boolean }) => Promise; + registerPreIntent: () => Promise; saveWorkspaceData: () => Promise; saveOrganizationData: () => Promise; completeSetupWizard: () => Promise; + offline: boolean; maxSteps: number; }; export const SetupWizardContext = createContext({ setupWizardData: { - adminData: { fullname: '', username: '', email: '', password: '' }, organizationData: { organizationName: '', organizationType: '', @@ -63,11 +63,13 @@ export const SetupWizardContext = createContext({ goToStep: () => undefined, registerAdminUser: async () => undefined, registerServer: async () => undefined, + registerPreIntent: async () => undefined, saveWorkspaceData: async () => undefined, saveOrganizationData: async () => undefined, validateEmail: () => true, currentStep: 1, completeSetupWizard: async () => undefined, + offline: false, maxSteps: 4, }); diff --git a/apps/meteor/client/views/setupWizard/providers/SetupWizardProvider.tsx b/apps/meteor/client/views/setupWizard/providers/SetupWizardProvider.tsx index 0fcb296ec623..4be0c9b8f858 100644 --- a/apps/meteor/client/views/setupWizard/providers/SetupWizardProvider.tsx +++ b/apps/meteor/client/views/setupWizard/providers/SetupWizardProvider.tsx @@ -5,7 +5,6 @@ import { useLoginWithPassword, useSettingSetValue, useSettingsDispatch, - useRole, useMethod, useEndpoint, useTranslation, @@ -22,7 +21,6 @@ import { useParameters } from '../hooks/useParameters'; import { useStepRouting } from '../hooks/useStepRouting'; const initialData: ContextType['setupWizardData'] = { - adminData: { fullname: '', username: '', email: '', password: '' }, organizationData: { organizationName: '', organizationType: '', @@ -43,10 +41,10 @@ type HandleRegisterServer = (params: { email: string; resend?: boolean }) => Pro const SetupWizardProvider = ({ children }: { children: ReactElement }): ReactElement => { const t = useTranslation(); - const hasAdminRole = useRole('admin'); const [setupWizardData, setSetupWizardData] = useState['setupWizardData']>(initialData); const [currentStep, setCurrentStep] = useStepRouting(); const { isSuccess, data } = useParameters(); + const [offline, setOffline] = useState(false); const dispatchToastMessage = useToastMessageDispatch(); const dispatchSettings = useSettingsDispatch(); @@ -55,6 +53,7 @@ const SetupWizardProvider = ({ children }: { children: ReactElement }): ReactEle const defineUsername = useMethod('setUsername'); const loginWithPassword = useLoginWithPassword(); const setForceLogin = useSessionDispatch('forceLogin'); + const registerPreIntentEndpoint = useEndpoint('POST', '/v1/cloud.registerPreIntent'); const createRegistrationIntent = useEndpoint('POST', '/v1/cloud.createRegistrationIntent'); const goToPreviousStep = useCallback(() => setCurrentStep((currentStep) => currentStep - 1), [setCurrentStep]); @@ -72,32 +71,32 @@ const SetupWizardProvider = ({ children }: { children: ReactElement }): ReactEle [t], ); - const registerAdminUser = useCallback(async (): Promise => { - const { - adminData: { fullname, username, email, password }, - } = setupWizardData; - await registerUser({ name: fullname, username, email, pass: password }); - callbacks.run('userRegistered', {}); + const registerAdminUser = useCallback( + async ({ fullname, username, email, password }): Promise => { + await registerUser({ name: fullname, username, email, pass: password }); + callbacks.run('userRegistered', {}); - try { - await loginWithPassword(email, password); - } catch (error) { - if (error instanceof Meteor.Error && error.error === 'error-invalid-email') { - dispatchToastMessage({ type: 'success', message: t('We_have_sent_registration_email') }); - return; - } - if (error instanceof Error || typeof error === 'string') { - dispatchToastMessage({ type: 'error', message: error }); + try { + await loginWithPassword(email, password); + } catch (error) { + if (error instanceof Meteor.Error && error.error === 'error-invalid-email') { + dispatchToastMessage({ type: 'success', message: t('We_have_sent_registration_email') }); + return; + } + if (error instanceof Error || typeof error === 'string') { + dispatchToastMessage({ type: 'error', message: error }); + } + throw error; } - throw error; - } - setForceLogin(false); + setForceLogin(false); - await defineUsername(username); - await dispatchSettings([{ _id: 'Organization_Email', value: email }]); - callbacks.run('usernameSet', {}); - }, [defineUsername, dispatchToastMessage, loginWithPassword, registerUser, setForceLogin, dispatchSettings, setupWizardData, t]); + await defineUsername(username); + await dispatchSettings([{ _id: 'Organization_Email', value: email }]); + callbacks.run('usernameSet', {}); + }, + [registerUser, setForceLogin, defineUsername, dispatchSettings, loginWithPassword, dispatchToastMessage, t], + ); const saveWorkspaceData = useCallback(async (): Promise => { const { @@ -158,18 +157,6 @@ const SetupWizardProvider = ({ children }: { children: ReactElement }): ReactEle }, [dispatchSettings, setupWizardData]); const registerServer: HandleRegisterServer = useMutableCallback(async ({ email, resend = false }): Promise => { - if (!hasAdminRole) { - try { - await registerAdminUser(); - } catch (e) { - if (e instanceof Error || typeof e === 'string') - return dispatchToastMessage({ - type: 'error', - message: e, - }); - } - } - try { await saveOrganizationData(); const { intentData } = await createRegistrationIntent({ resend, email }); @@ -188,10 +175,17 @@ const SetupWizardProvider = ({ children }: { children: ReactElement }): ReactEle } }); - const completeSetupWizard = useMutableCallback(async (): Promise => { - if (!hasAdminRole) { - await registerAdminUser(); + const registerPreIntent = useMutableCallback(async (): Promise => { + await saveOrganizationData(); + try { + const { offline } = await registerPreIntentEndpoint(); + setOffline(offline); + } catch (_) { + setOffline(true); } + }); + + const completeSetupWizard = useMutableCallback(async (): Promise => { await saveOrganizationData(); dispatchToastMessage({ type: 'success', message: t('Your_workspace_is_ready') }); return setShowSetupWizard('completed'); @@ -208,6 +202,8 @@ const SetupWizardProvider = ({ children }: { children: ReactElement }): ReactEle goToPreviousStep, goToNextStep, goToStep, + offline, + registerPreIntent, registerAdminUser, validateEmail: _validateEmail, registerServer, @@ -218,14 +214,16 @@ const SetupWizardProvider = ({ children }: { children: ReactElement }): ReactEle }), [ setupWizardData, - setSetupWizardData, currentStep, isSuccess, - registerAdminUser, - data, + data.settings, + data.serverAlreadyRegistered, goToPreviousStep, goToNextStep, goToStep, + offline, + registerAdminUser, + registerPreIntent, _validateEmail, registerServer, saveWorkspaceData, diff --git a/apps/meteor/client/views/setupWizard/steps/AdminInfoStep.tsx b/apps/meteor/client/views/setupWizard/steps/AdminInfoStep.tsx index 8da51ac859c9..1562c469bef9 100644 --- a/apps/meteor/client/views/setupWizard/steps/AdminInfoStep.tsx +++ b/apps/meteor/client/views/setupWizard/steps/AdminInfoStep.tsx @@ -10,14 +10,7 @@ const AdminInfoStep = (): ReactElement => { const regexpForUsernameValidation = useSetting('UTF8_User_Names_Validation'); const usernameRegExp = new RegExp(`^${regexpForUsernameValidation}$`); - const { - setupWizardData: { adminData }, - setSetupWizardData, - goToNextStep, - currentStep, - validateEmail, - maxSteps, - } = useSetupWizardContext(); + const { currentStep, validateEmail, registerAdminUser, maxSteps } = useSetupWizardContext(); // TODO: check if username exists const validateUsername = (username: string): boolean | string => { @@ -29,8 +22,7 @@ const AdminInfoStep = (): ReactElement => { }; const handleSubmit: ComponentProps['onSubmit'] = async (data) => { - setSetupWizardData((prevState) => ({ ...prevState, adminData: data })); - goToNextStep(); + registerAdminUser(data); }; return ( @@ -40,7 +32,6 @@ const AdminInfoStep = (): ReactElement => { validateUsername={validateUsername} validateEmail={validateEmail} currentStep={currentStep} - initialValues={adminData} stepCount={maxSteps} onSubmit={handleSubmit} /> diff --git a/apps/meteor/client/views/setupWizard/steps/OrganizationInfoStep.tsx b/apps/meteor/client/views/setupWizard/steps/OrganizationInfoStep.tsx index 41a304db40e1..73ea00c1aa44 100644 --- a/apps/meteor/client/views/setupWizard/steps/OrganizationInfoStep.tsx +++ b/apps/meteor/client/views/setupWizard/steps/OrganizationInfoStep.tsx @@ -37,6 +37,7 @@ const OrganizationInfoStep = (): ReactElement => { goToNextStep, completeSetupWizard, currentStep, + registerPreIntent, skipCloudRegistration, maxSteps, } = useSetupWizardContext(); @@ -51,6 +52,7 @@ const OrganizationInfoStep = (): ReactElement => { return completeSetupWizard(); } setSetupWizardData((prevState) => ({ ...prevState, organizationData: data })); + await registerPreIntent(); goToNextStep(); }; diff --git a/apps/meteor/client/views/setupWizard/steps/RegisterServerStep.tsx b/apps/meteor/client/views/setupWizard/steps/RegisterServerStep.tsx index 10dc50e154d6..6bc2e25259b6 100644 --- a/apps/meteor/client/views/setupWizard/steps/RegisterServerStep.tsx +++ b/apps/meteor/client/views/setupWizard/steps/RegisterServerStep.tsx @@ -1,4 +1,5 @@ -import { RegisteredServerPage, StandaloneServerPage } from '@rocket.chat/onboarding-ui'; +import { RegisterServerPage, StandaloneServerPage } from '@rocket.chat/onboarding-ui'; +import { useRoute } from '@rocket.chat/ui-contexts'; import type { ReactElement, ComponentProps } from 'react'; import React, { useState } from 'react'; @@ -10,18 +11,18 @@ const SERVER_OPTIONS = { }; const RegisterServerStep = (): ReactElement => { - const { - goToPreviousStep, - currentStep, - setSetupWizardData, - setupWizardData: { adminData }, - registerServer, - maxSteps, - completeSetupWizard, - } = useSetupWizardContext(); + const { goToPreviousStep, currentStep, setSetupWizardData, registerServer, maxSteps, offline, completeSetupWizard } = + useSetupWizardContext(); const [serverOption, setServerOption] = useState(SERVER_OPTIONS.REGISTERED); - const handleRegister: ComponentProps['onSubmit'] = async (data) => { + const router = useRoute('cloud'); + + const handleRegisterOffline: ComponentProps['onSubmit'] = async () => { + await completeSetupWizard(); + router.push({}, { register: 'true' }); + }; + + const handleRegister: ComponentProps['onSubmit'] = async (data) => { if (data.registerType !== 'standalone') { setSetupWizardData((prevState) => ({ ...prevState, serverData: data })); await registerServer(data); @@ -46,13 +47,13 @@ const RegisterServerStep = (): ReactElement => { } return ( - setServerOption(SERVER_OPTIONS.STANDALONE)} + setServerOption(SERVER_OPTIONS.STANDALONE)} onBackButtonClick={goToPreviousStep} stepCount={maxSteps} - onSubmit={handleRegister} + onSubmit={offline ? handleRegisterOffline : handleRegister} currentStep={currentStep} - initialValues={{ email: adminData.email }} + offline={offline} /> ); }; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 0592046ccfb6..95576e934995 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -5423,6 +5423,7 @@ "onboarding.component.form.action.next": "Next", "onboarding.component.form.action.skip": "Skip this step", "onboarding.component.form.action.register": "Register", + "onboarding.component.form.action.registerNow": "Register now", "onboarding.component.form.action.confirm": "Confirm", "onboarding.component.form.termsAndConditions": "I agree with <1>Terms and Conditions and <3>Privacy Policy", "onboarding.component.emailCodeFallback": "Didn’t receive email? <1>Resend or <3>Change email", @@ -5486,7 +5487,8 @@ "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "To register your server, we need to connect it to your cloud account. If you already have one - we will link it automatically. Otherwise, a new account will be created", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Please enter your Email", "onboarding.form.registeredServerForm.keepInformed": "Keep me informed about news and events", - "onboarding.form.registeredServerForm.continueStandalone": "Continue as standalone", + "onboarding.form.registeredServerForm.registerLater": "Register later", + "onboarding.form.registeredServerForm.notConnectedToInternet": "The server is not connected to the internet, so you’ll have to do an offline registration for this workspace.", "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "By registering I agree to receive relevant product and security updates", "onboarding.form.standaloneServerForm.title": "Standalone Server Confirmation", "onboarding.form.standaloneServerForm.servicesUnavailable": "Some of the services will be unavailable or will require manual setup", diff --git a/packages/rest-typings/src/v1/cloud.ts b/packages/rest-typings/src/v1/cloud.ts index 15a73d9da090..90664dcc243b 100644 --- a/packages/rest-typings/src/v1/cloud.ts +++ b/packages/rest-typings/src/v1/cloud.ts @@ -75,6 +75,11 @@ export type CloudEndpoints = { intentData: CloudRegistrationIntentData; }; }; + '/v1/cloud.registerPreIntent': { + POST: () => { + offline: boolean; + }; + }; '/v1/cloud.confirmationPoll': { GET: (params: CloudConfirmationPoll) => { pollData: CloudConfirmationPollData; diff --git a/yarn.lock b/yarn.lock index 3c767495350d..6f24e9cb899b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7272,8 +7272,8 @@ __metadata: linkType: hard "@rocket.chat/onboarding-ui@npm:next": - version: 0.32.0-dev.245 - resolution: "@rocket.chat/onboarding-ui@npm:0.32.0-dev.245" + version: 0.32.0-dev.275 + resolution: "@rocket.chat/onboarding-ui@npm:0.32.0-dev.275" dependencies: i18next: ~21.6.16 react-hook-form: ~7.27.1 @@ -7289,7 +7289,7 @@ __metadata: react: 17.0.2 react-dom: 17.0.2 react-i18next: ~11.15.4 - checksum: eef580b286ceab884e40c660269521e7a7a814803cd5b62a2b4968dcb4b41f180149dc97a708d79dd62ae5d27ddae2d9acadabf81e8cbdd6aae7bef1ed38873b + checksum: c5007a1e6dacd404e192327858a7b76da938380370df55da9ad51fdc055888d8fcc059117b21091826da02b043fd03aa3d0bcdc5eb573d7c330d7deef8edd196 languageName: node linkType: hard