[NEW] Workspace Manual Registration (#15442)

pull/15841/head
Guilherme Gazzo 6 years ago committed by Diego Sampaio
parent 376e37bfb6
commit e6a61d17e2
  1. 1
      app/api/server/index.js
  2. 30
      app/api/server/v1/cloud.js
  3. 9
      app/cloud/client/admin/cloud.html
  4. 13
      app/cloud/client/admin/cloud.js
  5. 26
      app/cloud/client/admin/cloudRegisterManually.css
  6. 36
      app/cloud/client/admin/cloudRegisterManually.html
  7. 106
      app/cloud/client/admin/cloudRegisterManually.js
  8. 4
      app/cloud/client/index.js
  9. 54
      app/cloud/server/functions/buildRegistrationData.js
  10. 9
      app/cloud/server/functions/connectWorkspace.js
  11. 22
      app/cloud/server/functions/saveRegistrationData.js
  12. 53
      app/cloud/server/functions/startRegisterWorkspace.js
  13. 12
      app/cloud/server/methods.js
  14. 4
      app/models/server/raw/BaseRaw.js
  15. 16
      app/models/server/raw/Settings.js
  16. 8
      app/theme/client/imports/components/modal.css
  17. 2
      app/theme/client/imports/forms/button.css
  18. 4
      app/theme/client/imports/general/reset.css
  19. 2
      app/theme/client/main.css
  20. 266
      package-lock.json
  21. 7
      packages/rocketchat-i18n/i18n/en.i18n.json

@ -13,6 +13,7 @@ import './default/info';
import './v1/assets';
import './v1/channels';
import './v1/chat';
import './v1/cloud';
import './v1/commands';
import './v1/e2e';
import './v1/emoji-custom';

@ -0,0 +1,30 @@
import { check } from 'meteor/check';
import { API } from '../api';
import { hasRole } from '../../../authorization';
import { saveRegistrationData } from '../../../cloud/server/functions/saveRegistrationData';
import { retrieveRegistrationStatus } from '../../../cloud/server/functions/retrieveRegistrationStatus';
API.v1.addRoute('cloud.manualRegister', { authRequired: true }, {
post() {
check(this.bodyParams, {
cloudBlob: String,
});
if (!hasRole(this.userId, 'admin')) {
return API.v1.unauthorized();
}
const registrationInfo = retrieveRegistrationStatus();
if (registrationInfo.connectToCloud) {
return API.v1.failure('Workspace is already registered');
}
const settingsData = JSON.parse(Buffer.from(this.bodyParams.cloudBlob, 'base64').toString());
Promise.await(saveRegistrationData(settingsData));
return API.v1.success();
},
});

@ -2,7 +2,14 @@
<div class="main-content-flex">
<section class="page-container page-home page-static page-settings">
{{#header sectionName="Connectivity_Services" hideHelp=true fixedHeight=true fullpage=true}}
<a href="https://cloud.rocket.chat" class="rc-button rc-button--primary action cloud-console-btn" target="_blank">{{_ "Cloud_console"}}</a>
<div class="rc-header__section-button">
{{#unless info.connectToCloud}}
<button class="rc-button rc-button--small rc-button--primary rc-button--outline js-register">
{{_ "Cloud_Register_manually"}}
</button>
{{/unless}}
<a href="https://cloud.rocket.chat" class="rc-button rc-button--primary action cloud-console-btn" target="_blank">{{_ "Cloud_console"}}</a>
</div>
{{/header}}
<div class="content">
{{#requiresPermission 'manage-cloud'}}

@ -8,7 +8,7 @@ import queryString from 'query-string';
import toastr from 'toastr';
import { t } from '../../../utils';
import { SideNav } from '../../../ui-utils/client';
import { SideNav, modal } from '../../../ui-utils/client';
Template.cloud.onCreated(function() {
@ -161,6 +161,17 @@ Template.cloud.helpers({
});
Template.cloud.events({
'click .js-register'() {
modal.open({
template: 'cloudRegisterManually',
showCancelButton: false,
showConfirmButton: false,
showFooter: false,
closeOnCancel: true,
html: true,
confirmOnEnter: false,
});
},
'click .update-email-btn'() {
const val = $('input[name=cloudEmail]').val();

@ -0,0 +1,26 @@
.rc-promtp {
display: flex;
min-height: 188px;
padding: 1rem;
border-radius: 2px;
background: #2f343d;
flex-flow: column wrap;
justify-content: space-between;
&--element,
&--element[disabled] {
flex: 1 1 auto;
resize: none;
color: #cbced1;
border: none;
background: none;
font-family: Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
font-size: 14px;
line-height: 20px;
}
}

@ -0,0 +1,36 @@
<template name="cloudRegisterManually">
{{> header sectionName="Cloud_Register_manually" hideHelp=true fullpage=true}}
{{# if copyStep }}
<form class="preferences-page__content">
<p class="rc-modal__description">{{_ "Cloud_register_offline_helper" }}</p>
<div class="rc-promtp">
<textarea class="rc-promtp--element" disabled>{{clientKey}}</textarea>
<button class="rc-button rc-button--primary js-copy" data-clipboard-text="{{clientKey}}">
{{>icon icon='copy'}} {{_ "Copy"}}
</button>
</div>
<p class="rc-modal__description js-cloud">{{#if cloudLink}} {{{cloudLink}}} {{else}} <a href="https://cloud.rocket.chat" rel="noopener noreferrer" class="cloud-console-btn" target="_blank"></a>{{/if}}</p>
</form>
<footer class="rc-modal__footer rc-modal__footer--empty">
<button class="rc-button rc-button--primary js-next">{{_ "Next"}}</button>
</footer>
{{else}}
<form class="preferences-page__content">
<p class="rc-modal__description">{{_ "Cloud_register_offline_finish_helper"}}</p>
<div class="rc-promtp">
<textarea class="js-cloud-key rc-promtp--element" placeholder="{{_ "Paste_here"}}" disabled={{isLoading}}></textarea>
</div>
</form>
<footer class="rc-modal__footer rc-modal__footer--empty">
<button class="rc-button rc-button--secondary js-back">{{_ "Back"}}</button>
<button class="rc-button rc-button--primary js-finish" disabled='{{disabled}}'>
{{#if isLoading}} {{> loading}} {{/if}}
<span style="{{#if isLoading}} visibility:hidden {{/if}}">{{_ "Finish Registration"}}</span>
</button>
</footer>
{{/if}}
</template>

@ -0,0 +1,106 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveDict } from 'meteor/reactive-dict';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import Clipboard from 'clipboard';
import toastr from 'toastr';
import { APIClient } from '../../../utils/client';
import { modal } from '../../../ui-utils/client';
import './cloudRegisterManually.html';
import './cloudRegisterManually.css';
const CLOUD_STEPS = {
COPY: 0,
PASTE: 1,
DONE: 2,
ERROR: 3,
};
Template.cloudRegisterManually.events({
'submit form'(e) {
e.preventDefault();
},
'input .js-cloud-key'(e, instance) {
instance.state.set('cloudKey', e.currentTarget.value);
},
'click .js-next'(event, instance) {
instance.state.set('step', CLOUD_STEPS.PASTE);
},
'click .js-back'(event, instance) {
instance.state.set('step', CLOUD_STEPS.COPY);
},
'click .js-finish'(event, instance) {
instance.state.set('loading', true);
APIClient
.post('v1/cloud.manualRegister', {}, { cloudBlob: instance.state.get('cloudKey') })
.then(() => modal.open({
type: 'success',
title: TAPi18n.__('Success'),
text: TAPi18n.__('Cloud_register_success'),
confirmButtonText: TAPi18n.__('Ok'),
closeOnConfirm: false,
showCancelButton: false,
}, () => window.location.reload()))
.catch(() => modal.open({
type: 'error',
title: TAPi18n.__('Error'),
text: TAPi18n.__('Cloud_register_error'),
}))
.then(() => instance.state.set('loading', false));
},
});
Template.cloudRegisterManually.helpers({
cloudLink() {
return Template.instance().cloudLink.get();
},
copyStep() {
return Template.instance().state.get('step') === CLOUD_STEPS.COPY;
},
clientKey() {
return Template.instance().state.get('clientKey');
},
isLoading() {
return Template.instance().state.get('loading');
},
step() {
return Template.instance().state.get('step');
},
disabled() {
const { state } = Template.instance();
const shouldDisable = state.get('cloudKey').trim().length === 0 || state.get('loading');
return shouldDisable && 'disabled';
},
});
Template.cloudRegisterManually.onRendered(function() {
const clipboard = new Clipboard('.js-copy');
clipboard.on('success', function() {
toastr.success(TAPi18n.__('Copied'));
});
const btn = this.find('.cloud-console-btn');
// After_copy_the_text_go_to_cloud
this.cloudLink.set(TAPi18n.__('Cloud_click_here').replace(/(\[(.*)\]\(\))/ig, (_, __, text) => btn.outerHTML.replace('</a>', `${ text }</a>`)));
});
Template.cloudRegisterManually.onCreated(function() {
this.cloudLink = new ReactiveVar();
this.state = new ReactiveDict({
step: CLOUD_STEPS.COPY,
loading: false,
clientKey: '',
cloudKey: '',
error: '',
});
Meteor.call('cloud:getWorkspaceRegisterData', (error, result) => {
this.state.set('clientKey', result);
});
});

@ -1,3 +1,7 @@
import './admin/callback';
import './admin/cloud';
import './admin/cloudRegisterManually';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { FlowRouter } from 'meteor/kadira:flow-router';

@ -0,0 +1,54 @@
import { settings } from '../../../settings';
import { Users } from '../../../models';
import { statistics } from '../../../statistics';
export function buildWorkspaceRegistrationData() {
const stats = statistics.get();
const address = settings.get('Site_Url');
const siteName = settings.get('Site_Name');
// If we have it lets send it because likely an update
const workspaceId = settings.get('Cloud_Workspace_Id');
const firstUser = Users.getOldest({ name: 1, emails: 1 });
const contactName = firstUser && firstUser.name;
let contactEmail = firstUser && firstUser.emails && firstUser.emails[0].address;
if (settings.get('Organization_Email')) {
contactEmail = settings.get('Organization_Email');
}
const allowMarketing = settings.get('Allow_Marketing_Emails');
const accountName = settings.get('Organization_Name');
const website = settings.get('Website');
const agreePrivacyTerms = settings.get('Cloud_Service_Agree_PrivacyTerms');
const { organizationType, industry, size: orgSize, country, language, serverType: workspaceType } = stats.wizard;
return {
uniqueId: stats.uniqueId,
workspaceId,
address,
contactName,
contactEmail,
allowMarketing,
accountName,
organizationType,
industry,
orgSize,
country,
language,
agreePrivacyTerms,
website,
siteName,
workspaceType,
deploymentMethod: stats.deploy.method,
deploymentPlatform: stats.deploy.platform,
version: stats.version,
setupComplete: true,
};
}

@ -6,6 +6,7 @@ import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';
import { getWorkspaceAccessToken } from './getWorkspaceAccessToken';
import { Settings } from '../../../models';
import { settings } from '../../../settings';
import { saveRegistrationData } from './saveRegistrationData';
export function connectWorkspace(token) {
const { connectToCloud } = retrieveRegistrationStatus();
@ -46,13 +47,7 @@ export function connectWorkspace(token) {
return false;
}
Settings.updateValueById('Cloud_Workspace_Id', data.workspaceId);
Settings.updateValueById('Cloud_Workspace_Name', data.client_name);
Settings.updateValueById('Cloud_Workspace_Client_Id', data.client_id);
Settings.updateValueById('Cloud_Workspace_Client_Secret', data.client_secret);
Settings.updateValueById('Cloud_Workspace_Client_Secret_Expires_At', data.client_secret_expires_at);
Settings.updateValueById('Cloud_Workspace_PublicKey', data.publicKey);
Settings.updateValueById('Cloud_Workspace_Registration_Client_Uri', data.registration_client_uri);
Promise.await(saveRegistrationData(data));
// Now that we have the client id and secret, let's get the access token
const accessToken = getWorkspaceAccessToken(true);

@ -0,0 +1,22 @@
import { Settings } from '../../../models/server/raw';
export function saveRegistrationData({
workspaceId,
client_name,
client_id,
client_secret,
client_secret_expires_at,
publicKey,
registration_client_uri,
}) {
return Promise.all([
Settings.updateValueById('Register_Server', true),
Settings.updateValueById('Cloud_Workspace_Id', workspaceId),
Settings.updateValueById('Cloud_Workspace_Name', client_name),
Settings.updateValueById('Cloud_Workspace_Client_Id', client_id),
Settings.updateValueById('Cloud_Workspace_Client_Secret', client_secret),
Settings.updateValueById('Cloud_Workspace_Client_Secret_Expires_At', client_secret_expires_at),
Settings.updateValueById('Cloud_Workspace_PublicKey', publicKey),
Settings.updateValueById('Cloud_Workspace_Registration_Client_Uri', registration_client_uri),
]);
}

@ -3,8 +3,8 @@ import { HTTP } from 'meteor/http';
import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';
import { syncWorkspace } from './syncWorkspace';
import { settings } from '../../../settings';
import { Settings, Users } from '../../../models';
import { statistics } from '../../../statistics';
import { Settings } from '../../../models';
import { buildWorkspaceRegistrationData } from './buildRegistrationData';
export function startRegisterWorkspace(resend = false) {
@ -22,54 +22,7 @@ export function startRegisterWorkspace(resend = false) {
}
}
const stats = statistics.get();
const address = settings.get('Site_Url');
const siteName = settings.get('Site_Name');
// If we have it lets send it because likely an update
const workspaceId = settings.get('Cloud_Workspace_Id');
const firstUser = Users.getOldest({ name: 1, emails: 1 });
const contactName = firstUser && firstUser.name;
let contactEmail = firstUser && firstUser.emails && firstUser.emails[0].address;
if (settings.get('Organization_Email')) {
contactEmail = settings.get('Organization_Email');
}
const allowMarketing = settings.get('Allow_Marketing_Emails');
const accountName = settings.get('Organization_Name');
const website = settings.get('Website');
const agreePrivacyTerms = settings.get('Cloud_Service_Agree_PrivacyTerms');
const { organizationType, industry, size: orgSize, country, language, serverType: workspaceType } = stats.wizard;
const regInfo = {
uniqueId: stats.uniqueId,
workspaceId,
address,
contactName,
contactEmail,
allowMarketing,
accountName,
organizationType,
industry,
orgSize,
country,
language,
agreePrivacyTerms,
website,
siteName,
workspaceType,
deploymentMethod: stats.deploy.method,
deploymentPlatform: stats.deploy.platform,
version: stats.version,
setupComplete: true,
};
const regInfo = buildWorkspaceRegistrationData();
const cloudUrl = settings.get('Cloud_Url');

@ -13,6 +13,7 @@ import { checkUserHasCloudLogin } from './functions/checkUserHasCloudLogin';
import { userLogout } from './functions/userLogout';
import { Settings } from '../../models';
import { hasPermission } from '../../authorization';
import { buildWorkspaceRegistrationData } from './functions/buildRegistrationData';
Meteor.methods({
'cloud:checkRegisterStatus'() {
@ -26,6 +27,17 @@ Meteor.methods({
return retrieveRegistrationStatus();
},
'cloud:getWorkspaceRegisterData'() {
if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:getWorkspaceRegisterData' });
}
if (!hasPermission(Meteor.userId(), 'manage-cloud')) {
throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'cloud:getWorkspaceRegisterData' });
}
return Buffer.from(JSON.stringify(buildWorkspaceRegistrationData())).toString('base64');
},
'cloud:registerWorkspace'() {
if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:startRegister' });

@ -18,4 +18,8 @@ export class BaseRaw {
find(...args) {
return this.col.find(...args);
}
update(...args) {
return this.col.update(...args);
}
}

@ -18,4 +18,20 @@ export class SettingsRaw extends BaseRaw {
return this.find(query);
}
updateValueById(_id, value) {
const query = {
blocked: { $ne: true },
value: { $ne: value },
_id,
};
const update = {
$set: {
value,
},
};
return this.col.update(query, update);
}
}

@ -7,6 +7,8 @@
height: auto;
max-height: 90%;
padding: 1.5rem;
animation: dropdown-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95);
border: none;
@ -42,6 +44,12 @@
font-weight: 500;
}
&__description {
font-size: 0.874rem;
margin-block-end: 1.5rem;
margin-block-start: 0.5rem;
}
&__title {
flex: 1 1 auto;

@ -194,6 +194,8 @@
margin: 10px -5px;
justify-content: flex-end;
& > .rc-button {
margin: 0 5px;
}

@ -139,3 +139,7 @@ table {
border-collapse: collapse;
}
a {
color: var(--rc-color-button-primary);
}

@ -1,6 +1,6 @@
/* General */
@import 'imports/general/reset.css';
@import 'imports/general/variables.css';
@import 'imports/general/reset.css';
@import 'imports/general/base_old.css';
@import 'imports/general/base.css';
@import 'imports/general/animations.css';

266
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -645,6 +645,12 @@
"Closed_by_visitor": "Closed by visitor",
"Closing_chat": "Closing chat",
"Cloud": "Cloud",
"Cloud_Register_manually": "Register Manually",
"Cloud_click_here": "After copy the text, go to cloud console. [Click here]()",
"Cloud_register_offline_finish_helper": "After completing the registration process in the Cloud Console you should be presented with some text. Please paste it here to finish the registration.",
"Cloud_register_offline_helper": "Workspaces can be manually registered if airgapped or network access is restricted. Copy the text below and go to our Cloud Console to complete the process.",
"Cloud_register_success": "Your workspace has been successfully registered!",
"Cloud_register_error": "There has been an error trying to process your request. Please try again later.",
"Cloud_connect": "Rocket.Chat Cloud Connect",
"Cloud_connect_support": "If you still haven't received a registration email please make sure your email is updated above. If you still have issues you can contact support at",
"Cloud_console": "Cloud Console",
@ -2428,6 +2434,7 @@
"Password_changed_successfully": "Password changed successfully",
"Password_Policy": "Password Policy",
"Past_Chats": "Past Chats",
"Paste_here": "Paste here...",
"Payload": "Payload",
"Peer_Password": "Peer Password",
"People": "People",

Loading…
Cancel
Save