Skip to content

Commit

Permalink
[IMPROVE] Setup Wizard Flow for airgapped environment (#28018)
Browse files Browse the repository at this point in the history
  • Loading branch information
gabriellsh authored Feb 14, 2023
1 parent 2e99a20 commit 1e1ad84
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 81 deletions.
15 changes: 15 additions & 0 deletions apps/meteor/app/api/server/v1/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 },
Expand Down
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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;
}
}
13 changes: 10 additions & 3 deletions apps/meteor/client/views/admin/cloud/CloudPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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');
Expand Down Expand Up @@ -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(<ManualWorkspaceRegistrationModal onClose={handleModalClose} />);
};
}, [setModal, reload]);

useEffect(() => {
if (shouldOpenManualRegistration) {
handleManualWorkspaceRegistrationButtonClick();
}
}, [shouldOpenManualRegistration, handleManualWorkspaceRegistrationButtonClick]);

if (result.isLoading || result.isError) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Parameters<ComponentProps<typeof AdminInfoPage>['onSubmit']>[0], 'keepPosted'>;
organizationData: Parameters<ComponentProps<typeof OrganizationInfoPage>['onSubmit']>[0];
serverData: Parameters<ComponentProps<typeof RegisteredServerPage>['onSubmit']>[0];
serverData: Parameters<ComponentProps<typeof RegisterServerPage>['onSubmit']>[0];
registrationData: {
device_code: string;
user_code: string;
Expand All @@ -28,17 +27,18 @@ type SetupWizarContextValue = {
goToPreviousStep: () => void;
goToNextStep: () => void;
goToStep: (step: number) => void;
registerAdminUser: () => Promise<void>;
registerAdminUser: (user: Omit<Parameters<ComponentProps<typeof AdminInfoPage>['onSubmit']>[0], 'keepPosted'>) => Promise<void>;
registerServer: (params: { email: string; resend?: boolean }) => Promise<void>;
registerPreIntent: () => Promise<void>;
saveWorkspaceData: () => Promise<void>;
saveOrganizationData: () => Promise<void>;
completeSetupWizard: () => Promise<void>;
offline: boolean;
maxSteps: number;
};

export const SetupWizardContext = createContext<SetupWizarContextValue>({
setupWizardData: {
adminData: { fullname: '', username: '', email: '', password: '' },
organizationData: {
organizationName: '',
organizationType: '',
Expand All @@ -63,11 +63,13 @@ export const SetupWizardContext = createContext<SetupWizarContextValue>({
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,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
useLoginWithPassword,
useSettingSetValue,
useSettingsDispatch,
useRole,
useMethod,
useEndpoint,
useTranslation,
Expand All @@ -22,7 +21,6 @@ import { useParameters } from '../hooks/useParameters';
import { useStepRouting } from '../hooks/useStepRouting';

const initialData: ContextType<typeof SetupWizardContext>['setupWizardData'] = {
adminData: { fullname: '', username: '', email: '', password: '' },
organizationData: {
organizationName: '',
organizationType: '',
Expand All @@ -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<ContextType<typeof SetupWizardContext>['setupWizardData']>(initialData);
const [currentStep, setCurrentStep] = useStepRouting();
const { isSuccess, data } = useParameters();
const [offline, setOffline] = useState(false);
const dispatchToastMessage = useToastMessageDispatch();
const dispatchSettings = useSettingsDispatch();

Expand All @@ -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]);
Expand All @@ -72,32 +71,32 @@ const SetupWizardProvider = ({ children }: { children: ReactElement }): ReactEle
[t],
);

const registerAdminUser = useCallback(async (): Promise<void> => {
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<void> => {
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<void> => {
const {
Expand Down Expand Up @@ -158,18 +157,6 @@ const SetupWizardProvider = ({ children }: { children: ReactElement }): ReactEle
}, [dispatchSettings, setupWizardData]);

const registerServer: HandleRegisterServer = useMutableCallback(async ({ email, resend = false }): Promise<void> => {
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 });
Expand All @@ -188,10 +175,17 @@ const SetupWizardProvider = ({ children }: { children: ReactElement }): ReactEle
}
});

const completeSetupWizard = useMutableCallback(async (): Promise<void> => {
if (!hasAdminRole) {
await registerAdminUser();
const registerPreIntent = useMutableCallback(async (): Promise<void> => {
await saveOrganizationData();
try {
const { offline } = await registerPreIntentEndpoint();
setOffline(offline);
} catch (_) {
setOffline(true);
}
});

const completeSetupWizard = useMutableCallback(async (): Promise<void> => {
await saveOrganizationData();
dispatchToastMessage({ type: 'success', message: t('Your_workspace_is_ready') });
return setShowSetupWizard('completed');
Expand All @@ -208,6 +202,8 @@ const SetupWizardProvider = ({ children }: { children: ReactElement }): ReactEle
goToPreviousStep,
goToNextStep,
goToStep,
offline,
registerPreIntent,
registerAdminUser,
validateEmail: _validateEmail,
registerServer,
Expand All @@ -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,
Expand Down
13 changes: 2 additions & 11 deletions apps/meteor/client/views/setupWizard/steps/AdminInfoStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -29,8 +22,7 @@ const AdminInfoStep = (): ReactElement => {
};

const handleSubmit: ComponentProps<typeof AdminInfoPage>['onSubmit'] = async (data) => {
setSetupWizardData((prevState) => ({ ...prevState, adminData: data }));
goToNextStep();
registerAdminUser(data);
};

return (
Expand All @@ -40,7 +32,6 @@ const AdminInfoStep = (): ReactElement => {
validateUsername={validateUsername}
validateEmail={validateEmail}
currentStep={currentStep}
initialValues={adminData}
stepCount={maxSteps}
onSubmit={handleSubmit}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const OrganizationInfoStep = (): ReactElement => {
goToNextStep,
completeSetupWizard,
currentStep,
registerPreIntent,
skipCloudRegistration,
maxSteps,
} = useSetupWizardContext();
Expand All @@ -51,6 +52,7 @@ const OrganizationInfoStep = (): ReactElement => {
return completeSetupWizard();
}
setSetupWizardData((prevState) => ({ ...prevState, organizationData: data }));
await registerPreIntent();
goToNextStep();
};

Expand Down
Loading

0 comments on commit 1e1ad84

Please sign in to comment.