[NEW] Marketplace integration with Rocket.Chat Cloud (#13809)

* Add the cloud token to the marketplace calls

* Switch the app downloading to the server

* Implement some of the buying an app iframe

* Show when apps are purchased in the list

* Revert the committed change in the marketplace base url

* More work on enabling purchasing

* Install Apps from marketplace

* Missing fixes on rebase

* Improve marketplace design

* Improve view of app’s logs

* Move buttons of app details to header

* Fix mispeling

* Fix purchase/download flow

* Enable checking for updates for an app based upon the version of the engine

* Disable a possible issue with installation of apps

* Perform app downloads with a special short lived token for app download only

* Fix imports

* Fix layout of app details on old chrome and safari

* Re enable upload button adding a setting for development mode

* Change download encoding to binary

* Add iframe loading

* Fix some UI problems
pull/13832/head^2
Rodrigo Nascimento 6 years ago committed by GitHub
parent 7c2d680b32
commit fd817b2b14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 77
      app/apps/assets/stylesheets/apps.css
  2. 51
      app/apps/client/admin/appLogs.html
  3. 55
      app/apps/client/admin/appLogs.js
  4. 23
      app/apps/client/admin/appManage.html
  5. 197
      app/apps/client/admin/appManage.js
  6. 74
      app/apps/client/admin/apps.html
  7. 128
      app/apps/client/admin/apps.js
  8. 7
      app/apps/client/admin/modalTemplates/iframeModal.html
  9. 48
      app/apps/client/admin/modalTemplates/iframeModal.js
  10. 3
      app/apps/client/index.js
  11. 6
      app/apps/server/bridges/api.js
  12. 12
      app/apps/server/bridges/commands.js
  13. 6
      app/apps/server/bridges/environmental.js
  14. 2
      app/apps/server/bridges/http.js
  15. 8
      app/apps/server/bridges/listeners.js
  16. 14
      app/apps/server/bridges/messages.js
  17. 18
      app/apps/server/bridges/persistence.js
  18. 16
      app/apps/server/bridges/rooms.js
  19. 12
      app/apps/server/bridges/settings.js
  20. 4
      app/apps/server/bridges/users.js
  21. 172
      app/apps/server/communication/rest.js
  22. 28
      app/apps/server/orchestrator.js
  23. 16
      app/cloud/server/functions/getWorkspaceAccessToken.js
  24. 5
      app/theme/client/imports/components/modal/directory.css
  25. 6
      app/ui/client/components/header/header.html
  26. 10
      packages/rocketchat-i18n/i18n/en.i18n.json

@ -83,6 +83,7 @@
}
.rc-apps-container {
margin-top: 0;
padding-bottom: 15px;
}
@ -97,9 +98,20 @@
margin-top: 6px;
} */
.rc-header .rc-button {
min-height: 0;
margin: 0;
.content {
/* display: block !important; */
padding: 0 !important;
> .rc-apps-container {
display: block;
overflow-y: scroll;
padding: 0 !important;
}
> .rc-apps-details {
display: block;
}
}
.rc-apps-category {
@ -143,19 +155,56 @@
}
.rc-table-info {
height: 40px;
margin: 0 7px;
}
.rc-app-price {
position: relative;
top: -3px;
}
.rc-table-td--medium {
width: 300px;
}
.rc-table td {
padding: 0.5rem 0;
padding-right: 10px;
}
td.rc-apps-marketplace-price {
text-align: right;
button {
font-weight: 600;
}
.rc-icon {
color: #3582f3;
}
}
th.rc-apps-marketplace-price {
width: 120px;
}
&__wrap-actions {
& > .rc-icon--loading {
& > .loading {
display: none;
}
&.loading {
& > .rc-icon--loading {
& > .loading {
display: block;
animation: spin 1s linear infinite;
font-size: 11px;
font-weight: 600;
& > .rc-icon--loading {
animation: spin 1s linear infinite;
}
}
& > .apps-installer {
@ -163,6 +212,22 @@
}
}
}
.arrow-up {
transform: rotate(180deg);
}
&.page-settings {
.section {
padding: 0 !important;
border-bottom: none !important;
&:hover {
background-color: var(--rc-color-primary-lightest);
}
}
}
}
@keyframes play90 {

@ -1,24 +1,21 @@
<template name="appLogs">
{{#with app}}
<section class="page-container page-list page-settings flex-tab-main-content">
<header class="fixed-title">
{{> burger}}
<a href="{{pathFor "app-manage" appId=id}}" title="{{_ "Back_to_Manage_Apps"}}">
<i class="icon-left-open"></i>
</a> &nbsp;
<section class="page-container page-list page-settings flex-tab-main-content rc-apps-marketplace">
{{# header rawSectionName=title fixedHeight=true hideHelp=true fullpage=true}}
<div class="rc-header__block rc-header__block-action">
<button class="rc-button rc-button--primary js-refresh">
{{> icon icon="reload" block="rc-icon--default-size"}} {{_ "Refresh"}}
</button>
<button class="rc-button rc-button--nude js-cancel">{{> icon icon="cross"}}</button>
{{#if isReady}}
<h2>
<span class="room-title">{{_ "View_the_Logs_for" name=name}}</span>
</h2>
{{/if}}
</div>
{{/header}}
<div class="content">
{{#if hasError}}
<h2>
<span class="room-title">{{ theError }}</span>
</h2>
{{/if}}
</header>
<div class="content">
{{#if isReady}}
<div class="rocket-form">
{{#each log in logs}}
@ -28,30 +25,18 @@
{{formatDate log._createdAt}}: "{{log.method}}" ({{log.totalTime}}ms)
</div>
<div class="section-title-right">
<button class="button primary expand">
<span>{{_ "Expand"}}</span>
<button class="rc-button rc-button--nude button-down">
{{> icon icon="arrow-down" block="rc-icon--default-size"}}
</button>
</div>
</div>
<div class="section-content">
<h4>General Information</h4>
<ul>
<li>Method: {{ log.method }}</li>
<li>Start Time: {{ log.startTime }}</li>
<li>End Time: {{ log.endTime }}</li>
<li>Total Time: {{ log.totalTime }}ms</li>
<li>Log Entries: {{ log.entries.length }}</li>
</ul>
<h4>Log Entries</h4>
<ul>
{{#each entry in log.entries}}
<li>Timestamp: {{ entry.timestamp }}</li>
<li>Severity: {{ entry.severity }}</li>
<li>Caller: {{ entry.caller }}</li>
<li>Arguments: <pre><code class="code-colors hljs json">{{{ jsonStringify entry.args }}}</code></pre></li>
{{/each}}
</ul>
{{#each entry in log.entries}}
<div>{{ entry.severity }}: {{ entry.timestamp }} (Caller: {{ entry.caller }})</div>
<div>
<pre><code class="code-colors hljs json">{{{ jsonStringify entry.args }}}</code></pre>
</div>
{{/each}}
</div>
</div>
{{/each}}

@ -6,30 +6,32 @@ import { APIClient } from '../../../utils';
import moment from 'moment';
import hljs from 'highlight.js';
Template.appLogs.onCreated(function() {
const instance = this;
this.id = new ReactiveVar(FlowRouter.getParam('appId'));
this.ready = new ReactiveVar(false);
this.hasError = new ReactiveVar(false);
this.theError = new ReactiveVar('');
this.app = new ReactiveVar({});
this.logs = new ReactiveVar([]);
const id = this.id.get();
const loadData = (instance) => {
Promise.all([
APIClient.get(`apps/${ id }`),
APIClient.get(`apps/${ id }/logs`),
APIClient.get(`apps/${ instance.id.get() }`),
APIClient.get(`apps/${ instance.id.get() }/logs`),
]).then((results) => {
instance.app.set(results[0].app);
instance.logs.set(results[1].logs);
this.ready.set(true);
instance.ready.set(true);
}).catch((e) => {
instance.hasError.set(true);
instance.theError.set(e.message);
});
};
Template.appLogs.onCreated(function() {
const instance = this;
this.id = new ReactiveVar(FlowRouter.getParam('appId'));
this.ready = new ReactiveVar(false);
this.hasError = new ReactiveVar(false);
this.theError = new ReactiveVar('');
this.app = new ReactiveVar({});
this.logs = new ReactiveVar([]);
loadData(instance);
});
Template.appLogs.helpers({
@ -76,16 +78,29 @@ Template.appLogs.helpers({
return value.replace(/\\\\n/g, '<br>');
},
title() {
return TAPi18n.__('View_the_Logs_for', { name: Template.instance().app.get().name });
},
});
Template.appLogs.events({
'click .expand': (e) => {
$(e.currentTarget).closest('.section').removeClass('section-collapsed');
$(e.currentTarget).closest('button').removeClass('expand').addClass('collapse').find('span').text(TAPi18n.__('Collapse'));
'click .section-collapsed .section-title': (e) => {
$(e.currentTarget).closest('.section').removeClass('section-collapsed').addClass('section-expanded');
$(e.currentTarget).find('.button-down').addClass('arrow-up');
},
'click .section-expanded .section-title': (e) => {
$(e.currentTarget).closest('.section').removeClass('section-expanded').addClass('section-collapsed');
$(e.currentTarget).find('.button-down').removeClass('arrow-up');
},
'click .js-cancel': (e, t) => {
FlowRouter.go('app-manage', { appId: t.app.get().id }, { version: FlowRouter.getQueryParam('version') });
},
'click .collapse': (e) => {
$(e.currentTarget).closest('.section').addClass('section-collapsed');
$(e.currentTarget).closest('button').addClass('expand').removeClass('collapse').find('span').text(TAPi18n.__('Expand'));
'click .js-refresh': (e, t) => {
t.ready.set(false);
t.logs.set([]);
loadData(t);
},
});

@ -3,6 +3,13 @@
{{#with app}}
<section class="page-container page-home page-static page-settings rc-apps-marketplace">
{{# header sectionName='App_Details' fixedHeight=true hideHelp=true fullpage=true}}
<div class="rc-header__section-button">
{{#unless disabled}}
<button class="rc-button rc-button--cancel js-cancel-editing">{{_ "Cancel" }}</button>
{{/unless}}
<button class="rc-button rc-button--primary js-save {{#if saving}} loading{{/if}}" disabled='{{disabled}}'>{{_ "Save_changes" }}</button>
</div>
<div class="rc-header__block rc-header__block-action">
<button class="rc-button rc-button--nude js-cancel">{{> icon icon="cross"}}</button>
</div>
@ -18,7 +25,7 @@
<div class="rc-apps-details">
<div class="rc-apps-container rc-apps-container__header">
{{#if iconFileData}}
<div class="rc-apps-details__photo" style="background-image:url(data:image/png;base64,{{iconFileData}})"></div>
<div class="rc-apps-details__photo" style="background-image:url(data:image/png;base64,{{iconFileData}})"></div>
{{else}}
<div class="rc-apps-details__photo" style="background-image:url({{iconFileContent}})"></div>
{{/if}}
@ -44,7 +51,15 @@
{{/if}}
<button class="rc-button rc-button--nude js-view-logs">{{> icon icon="list-alt"}} {{_ "View_Logs" }}</button>
{{else}}
<button class="rc-button rc-button--primary js-install">{{> icon icon="circled-arrow-down"}} {{_ "Install"}}</button>
{{#if hasPurchased}}
<button class="rc-button rc-button--primary js-install">{{> icon icon="download"}} {{_ "Purchased"}}</button>
{{else}}
{{#if $eq price 0}}
<button class="rc-button rc-button--primary js-purchase">{{> icon icon="circled-arrow-down"}} {{_ "Free"}}</button>
{{else}}
<button class="rc-button rc-button--primary js-purchase">{{> icon icon="circled-arrow-down"}} {{displayPrice}}</button>
{{/if}}
{{/if}}
{{/if}}
</div>
</div>
@ -454,10 +469,6 @@
</div>
{{/each}}
<div class="rc-button-group">
<button class="rc-button rc-button--outline js-cancel-editing" disabled='{{disabled}}'>{{_ "Cancel" }}</button>
<button class="rc-button rc-button--primary js-save {{#if saving}} loading{{/if}}" disabled='{{disabled}}'>{{_ "Save" }}</button>
</div>
</div>
{{/if}}
</div>

@ -4,9 +4,10 @@ import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/tap:i18n';
import { TAPi18next } from 'meteor/tap:i18n';
import { isEmail, Info, APIClient } from '../../../utils';
import { isEmail, APIClient } from '../../../utils';
import { settings } from '../../../settings';
import { Markdown } from '../../../markdown/client';
import { modal } from '../../../ui-utils';
import _ from 'underscore';
import s from 'underscore.string';
import toastr from 'toastr';
@ -16,55 +17,111 @@ import { Utilities } from '../../lib/misc/Utilities';
import { Apps } from '../orchestrator';
import semver from 'semver';
const HOST = 'https://marketplace.rocket.chat'; // TODO move this to inside RocketChat.API
async function getApps(instance) {
function getApps(instance) {
const id = instance.id.get();
let remoteApps;
let localApp;
try {
localApp = (await APIClient.get('apps/')).apps.filter((app) => app.id === id)[0];
remoteApps = await fetch(`${ HOST }/v1/apps/${ id }?version=${ Info.marketplaceApiVersion }`).then((data) => data.json());
} catch (error) {
if (!localApp) {
const appInfo = { remote: undefined, local: undefined };
return APIClient.get(`apps/${ id }?marketplace=true&version=${ FlowRouter.getQueryParam('version') }`)
.catch((e) => {
console.log(e);
return Promise.resolve({ app: undefined });
})
.then((remote) => {
appInfo.remote = remote.app;
return APIClient.get(`apps/${ id }`);
})
.then((local) => {
appInfo.local = local.app;
return Apps.getAppApis(id);
})
.then((apis) => instance.apis.set(apis))
.catch((e) => {
if (appInfo.remote || appInfo.local) {
return Promise.resolve(true);
}
instance.hasError.set(true);
instance.theError.set(error.message);
}
}
let remoteApp;
if (remoteApps && remoteApps.length) {
remoteApps = remoteApps.sort((a, b) => {
if (semver.gt(a.version, b.version)) {
return -1;
instance.theError.set(e.message);
}).then((goOn) => {
if (typeof goOn !== 'undefined' && !goOn) {
return;
}
if (semver.lt(a.version, b.version)) {
return 1;
if (appInfo.remote) {
appInfo.remote.displayPrice = parseFloat(appInfo.remote.price).toFixed(2);
}
return 0;
});
remoteApp = remoteApps[0];
}
if (localApp) {
localApp.installed = true;
if (remoteApp) {
localApp.categories = remoteApp.categories;
if (semver.gt(remoteApp.version, localApp.version)) {
localApp.newVersion = remoteApp.version;
if (appInfo.local) {
appInfo.local.installed = true;
if (appInfo.remote) {
appInfo.local.categories = appInfo.remote.categories;
appInfo.local.isPurchased = appInfo.remote.isPurchased;
appInfo.local.price = appInfo.remote.price;
appInfo.local.displayPrice = appInfo.remote.displayPrice;
if (semver.gt(appInfo.remote.version, appInfo.local.version) && (appInfo.remote.isPurchased || appInfo.remote.price <= 0)) {
appInfo.local.newVersion = appInfo.remote.version;
}
}
instance.onSettingUpdated({ appId: id });
Apps.getWsListener().unregisterListener(AppEvents.APP_STATUS_CHANGE, instance.onStatusChanged);
Apps.getWsListener().unregisterListener(AppEvents.APP_SETTING_UPDATED, instance.onSettingUpdated);
Apps.getWsListener().registerListener(AppEvents.APP_STATUS_CHANGE, instance.onStatusChanged);
Apps.getWsListener().registerListener(AppEvents.APP_SETTING_UPDATED, instance.onSettingUpdated);
}
}
instance.onSettingUpdated({ appId: id });
instance.app.set(appInfo.local || appInfo.remote);
instance.ready.set(true);
Apps.getWsListener().unregisterListener(AppEvents.APP_STATUS_CHANGE, instance.onStatusChanged);
Apps.getWsListener().unregisterListener(AppEvents.APP_SETTING_UPDATED, instance.onSettingUpdated);
Apps.getWsListener().registerListener(AppEvents.APP_STATUS_CHANGE, instance.onStatusChanged);
Apps.getWsListener().registerListener(AppEvents.APP_SETTING_UPDATED, instance.onSettingUpdated);
}
if (appInfo.remote && appInfo.local) {
return APIClient.get(`apps/${ id }?marketplace=true&update=true&appVersion=${ FlowRouter.getQueryParam('version') }`);
}
return Promise.resolve(false);
}).then((updateInfo) => {
if (!updateInfo) {
return;
}
const update = updateInfo.app;
instance.app.set(localApp || remoteApp);
if (semver.gt(update.version, appInfo.local.version) && (update.isPurchased || update.price <= 0)) {
appInfo.local.newVersion = update.version;
instance.ready.set(true);
instance.app.set(appInfo.local);
}
});
}
function installAppFromEvent(e, t) {
const el = $(e.currentTarget);
el.prop('disabled', true);
el.addClass('loading');
const app = t.app.get();
const api = app.newVersion ? `apps/${ t.id.get() }` : 'apps/';
APIClient.post(api, {
appId: app.id,
marketplace: true,
version: app.version,
}).then(() => getApps(t)).then(() => {
el.prop('disabled', false);
el.removeClass('loading');
}).catch((e) => {
el.prop('disabled', false);
el.removeClass('loading');
t.hasError.set(true);
t.theError.set((e.xhr.responseJSON && e.xhr.responseJSON.error) || e.message);
});
// play animation
// TODO this icon and animation are not working
$(e.currentTarget).find('.rc-icon').addClass('play');
}
Template.appManage.onCreated(function() {
@ -81,12 +138,7 @@ Template.appManage.onCreated(function() {
this.loading = new ReactiveVar(false);
const id = this.id.get();
this.getApis = async() => {
this.apis.set(await Apps.getAppApis(id));
};
this.getApis();
getApps(instance);
this.__ = (key, options, lang_tag) => {
const appKey = Utilities.getI18nKeyForApp(key, id);
@ -104,8 +156,6 @@ Template.appManage.onCreated(function() {
instance.settings.set(settings);
}
getApps(instance);
instance.onStatusChanged = function _onStatusChanged({ appId, status }) {
if (appId !== id) {
return;
@ -223,6 +273,11 @@ Template.appManage.helpers({
return instance.app.get().installed === true;
},
hasPurchased() {
const instance = Template.instance();
return instance.app.get().isPurchased === true;
},
app() {
return Template.instance().app.get();
},
@ -318,31 +373,27 @@ Template.appManage.events({
},
'click .js-install': async(e, t) => {
const el = $(e.currentTarget);
el.prop('disabled', true);
el.addClass('loading');
const app = t.app.get();
const url = `${ HOST }/v1/apps/${ t.id.get() }/download/${ app.version }`;
const api = app.newVersion ? `apps/${ t.id.get() }` : 'apps/';
APIClient.post(api, { url }).then(() => {
getApps(t).then(() => {
el.prop('disabled', false);
el.removeClass('loading');
installAppFromEvent(e, t);
},
'click .js-purchase': (e, t) => {
const rl = t.app.get();
APIClient.get(`apps?buildBuyUrl=true&appId=${ rl.id }`)
.then((data) => {
data.successCallback = async() => {
installAppFromEvent(e, t);
};
modal.open({
allowOutsideClick: false,
data,
template: 'iframeModal',
});
})
.catch((e) => {
toastr.error((e.xhr.responseJSON && e.xhr.responseJSON.error) || e.message);
});
}).catch((e) => {
el.prop('disabled', false);
el.removeClass('loading');
t.hasError.set(true);
t.theError.set((e.xhr.responseJSON && e.xhr.responseJSON.error) || e.message);
});
// play animation
// TODO this icon and animation are not working
$(e.currentTarget).find('.rc-icon').addClass('play');
},
'click .js-update': (e, t) => {
@ -350,7 +401,7 @@ Template.appManage.events({
},
'click .js-view-logs': (e, t) => {
FlowRouter.go(`/admin/apps/${ t.id.get() }/logs`);
FlowRouter.go(`/admin/apps/${ t.id.get() }/logs`, {}, { version: FlowRouter.getQueryParam('version') });
},
'click .js-cancel-editing': async(e, t) => {

@ -1,7 +1,9 @@
<template name="apps">
<section class="rc-directory rc-apps-marketplace">
{{#header sectionName="Apps" hideHelp=true fixedHeight=true fullpage=true}}
<button class="rc-button rc-button--small rc-button--primary rc-directory-plus" data-button="install">{{> icon icon="plus"}}</button>
{{#if appsDevelopmentMode}}
<button class="rc-button rc-button--small rc-button--primary rc-button--outline rc-directory-plus" data-button="install">{{> icon icon="upload" block="rc-icon--default-size"}} {{_ "Upload app"}}</button>
{{/if}}
{{/header}}
<div class="rc-table-content">
{{>tabs tabs=tabsData}}
@ -20,13 +22,10 @@
<th class="js-sort rc-table-td--medium {{#if searchSortBy 'name'}}is-sorting{{/if}}" data-sort="name">
<div class="table-fake-th">{{_ "Name"}} {{> icon icon=(sortIcon 'name')}}</div>
</th>
<th class="js-sort rc-table-td--medium {{#if searchSortBy 'category'}}is-sorting{{/if}}" data-sort="category">>
<div class="table-fake-th">{{_ "Category"}} {{> icon icon=(sortIcon 'category') }}</div>
</th>
<th class="rc-table-td">
<div class="table-fake-th">{{_ "Details"}} </div>
</th>
<th class="rc-table-td--small">
<th class="rc-table-td--small rc-apps-marketplace-price">
</th>
</tr>
</thead>
@ -50,32 +49,57 @@
</div>
</div>
</td>
<td>{{latest.categories}}</td>
<td>
<p class="rc-table-title">
{{#if latest.summary}}
{{latest.summary}}
{{else}}
{{latest.description}}
{{/if}}
</p>
{{#if latest.summary}}
<p>
{{latest.description}}
</p>
{{/if}}
<div class="rc-table-wrapper">
<div class="rc-table-info">
<span class="rc-table-title">
{{#if latest.summary}}
{{latest.summary}}
{{else}}
{{latest.description}}
{{/if}}
</span>
{{#if latest.summary}}
<span class="rc-table-subtitle">
{{latest.description}}
</span>
{{/if}}
<span class="rc-table-subtitle">
{{formatCategories latest.categories}}
</span>
</div>
</div>
</td>
<td>
<td class="rc-apps-marketplace-price">
{{#if $eq latest._installed true}}
{{> icon icon="checkmark-circled" block="rc-icon--default-size"}}
<button class="apps-installer" data-app="{{appId}}">
{{_ "Installed"}}
{{> icon icon="menu" block="rc-icon--default-size"}}
</button>
{{/if}}
{{#if renderDownloadButton latest}}
<div class="rc-apps-marketplace__wrap-actions">
{{> icon block="rc-icon--default-size rc-icon" icon="loading"}}
<button class="js-install apps-installer" data-app="{{appId}}">
{{> icon block="installer rc-icon--default-size" icon="circled-arrow-down"}}
</button>
{{#if $eq isPurchased true}}
<button class="js-install apps-installer" data-app="{{appId}}">
{{_ "Purchased"}}
{{> icon block="installer rc-icon--default-size" icon="download"}}
</button>
{{else}}
<button class="js-purchase apps-installer" data-app="{{appId}}">
<span class="rc-app-price">
{{#if $eq price 0}}
{{_ "Free"}}
{{else}}
{{ formatPrice price }}
{{/if}}
</span>
{{> icon block="installer rc-icon--default-size" icon="circled-arrow-down"}}
</button>
{{/if}}
<span class="loading">
Downloading
{{> icon block="rc-icon--default-size rc-icon" icon="loading"}}
</span>
</div>
{{/if}}
</td>

@ -2,12 +2,13 @@ import toastr from 'toastr';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import { t, Info, APIClient } from '../../../utils';
import { settings } from '../../../settings';
import { t, APIClient } from '../../../utils';
import { modal } from '../../../ui-utils';
import { AppEvents } from '../communication';
import { Apps } from '../orchestrator';
const ENABLED_STATUS = ['auto_enabled', 'manually_enabled'];
const HOST = 'https://marketplace.rocket.chat';
const enabled = ({ status }) => ENABLED_STATUS.includes(status);
const sortByColumn = (array, column, inverted) =>
@ -23,6 +24,8 @@ const tagAlreadyInstalledApps = (installedApps, apps) => {
const tagged = apps.map((app) =>
({
price: app.price,
isPurchased: app.isPurchased,
latest: {
...app.latest,
_installed: installedIds.includes(app.latest.id),
@ -33,31 +36,29 @@ const tagAlreadyInstalledApps = (installedApps, apps) => {
return tagged;
};
const getApps = (instance) => {
fetch(`${ HOST }/v1/apps?version=${ Info.marketplaceApiVersion }`)
.then((response) => response.json())
.then((data) => {
const tagged = tagAlreadyInstalledApps(instance.installedApps.get(), data);
const getApps = async(instance) => {
instance.isLoading.set(true);
if (instance.searchType.get() === 'marketplace') {
instance.apps.set(tagged);
instance.isLoading.set(false);
instance.ready.set(true);
}
});
const data = await APIClient.get('apps?marketplace=true');
const tagged = tagAlreadyInstalledApps(instance.installedApps.get(), data);
if (instance.searchType.get() === 'marketplace') {
instance.apps.set(tagged);
instance.isLoading.set(false);
instance.ready.set(true);
}
};
const getInstalledApps = (instance) => {
APIClient.get('apps').then((data) => {
const apps = data.apps.map((app) => ({ latest: app }));
instance.installedApps.set(apps);
const getInstalledApps = async(instance) => {
const data = await APIClient.get('apps');
const apps = data.apps.map((app) => ({ latest: app }));
instance.installedApps.set(apps);
if (instance.searchType.get() === 'installed') {
instance.apps.set(apps);
instance.isLoading.set(false);
instance.ready.set(true);
}
});
if (instance.searchType.get() === 'installed') {
instance.apps.set(apps);
instance.isLoading.set(false);
instance.ready.set(true);
}
};
Template.apps.onCreated(function() {
@ -82,14 +83,10 @@ Template.apps.onCreated(function() {
}
}
getApps(instance);
getInstalledApps(instance);
getApps(instance);
fetch(`${ HOST }/v1/categories`)
.then((response) => response.json())
.then((data) => {
instance.categories.set(data);
});
APIClient.get('apps?categories=true').then((data) => instance.categories.set(data));
instance.onAppAdded = function _appOnAppAdded() {
// ToDo: fix this formatting data to add an app to installedApps array without to fetch all
@ -149,6 +146,9 @@ Template.apps.helpers({
categories() {
return Template.instance().categories.get();
},
appsDevelopmentMode() {
return settings.get('Apps_Framework_Development_Mode') === true;
},
parseStatus(status) {
return t(`App_status_${ status }`);
},
@ -211,6 +211,12 @@ Template.apps.helpers({
return isMarketplace && isDownloaded;
},
formatPrice(price) {
return `$${ Number.parseFloat(price).toFixed(2) }`;
},
formatCategories(categories = []) {
return categories.join(', ');
},
tabsData() {
const instance = Template.instance();
@ -256,7 +262,7 @@ Template.apps.events({
const rl = this;
if (rl && rl.latest && rl.latest.id) {
FlowRouter.go(`/admin/apps/${ rl.latest.id }`);
FlowRouter.go(`/admin/apps/${ rl.latest.id }?version=${ rl.latest.version }`);
}
},
'click [data-button="install"]'() {
@ -264,18 +270,64 @@ Template.apps.events({
},
'click .js-install'(e, template) {
e.stopPropagation();
const elm = e.currentTarget.parentElement;
const url = `${ HOST }/v1/apps/${ this.latest.id }/download/${ this.latest.version }`;
elm.classList.add('loading');
// play animation
e.currentTarget.parentElement.classList.add('loading');
APIClient.post('apps/', {
appId: this.latest.id,
marketplace: true,
version: this.latest.version,
})
.then(async() => {
await Promise.all([
getInstalledApps(template),
getApps(template),
]);
elm.classList.remove('loading');
})
.catch((e) => {
toastr.error((e.xhr.responseJSON && e.xhr.responseJSON.error) || e.message);
elm.classList.remove('loading');
});
},
'click .js-purchase'(e, template) {
e.stopPropagation();
APIClient.post('apps/', { url })
.then(() => {
getApps(template);
getInstalledApps(template);
const rl = this;
// play animation
const elm = e.currentTarget.parentElement;
APIClient.get(`apps?buildBuyUrl=true&appId=${ rl.latest.id }`)
.then((data) => {
modal.open({
allowOutsideClick: false,
data,
template: 'iframeModal',
}, () => {
elm.classList.add('loading');
APIClient.post('apps/', {
appId: this.latest.id,
marketplace: true,
version: this.latest.version,
})
.then(async() => {
await Promise.all([
getInstalledApps(template),
getApps(template),
]);
elm.classList.remove('loading');
})
.catch((e) => {
toastr.error((e.xhr.responseJSON && e.xhr.responseJSON.error) || e.message);
elm.classList.remove('loading');
});
});
})
.catch((e) => toastr.error((e.xhr.responseJSON && e.xhr.responseJSON.error) || e.message));
.catch((e) => {
toastr.error((e.xhr.responseJSON && e.xhr.responseJSON.error) || e.message);
});
},
'keyup .js-search'(e, t) {
t.searchText.set(e.currentTarget.value);

@ -0,0 +1,7 @@
<template name="iframeModal">
<main class="rc-modal__content" style="height: 300px; width: 450px;">
<iframe src="{{ data.url }}" style="border: none; display: none; height: 100%; width: 100%;"></iframe>
<div class="loading">{{> loading class="loading-animation--primary"}}</div>
</main>
</template>

@ -0,0 +1,48 @@
import { Template } from 'meteor/templating';
import { modal } from '../../../../ui-utils';
Template.iframeModal.onCreated(function() {
const instance = this;
instance.iframeMsgListener = function _iframeMsgListener(e) {
let data;
try {
data = JSON.parse(e.data);
} catch (e) {
return;
}
if (data.result) {
if (typeof instance.data.successCallback === 'function') {
instance.data.successCallback().then(() => modal.confirm(data));
} else {
modal.confirm(data);
}
} else {
modal.cancel();
}
};
window.addEventListener('message', instance.iframeMsgListener);
});
Template.iframeModal.onRendered(function() {
const iframe = this.firstNode.querySelector('iframe');
const loading = this.firstNode.querySelector('.loading');
iframe.addEventListener('load', () => {
iframe.style.display = 'block';
loading.style.display = 'none';
});
});
Template.iframeModal.onDestroyed(function() {
const instance = this;
window.removeEventListener('message', instance.iframeMsgListener);
});
Template.iframeModal.helpers({
data() {
return Template.instance().data;
},
});

@ -1,4 +1,7 @@
export { Apps } from './orchestrator';
import './admin/modalTemplates/iframeModal.html';
import './admin/modalTemplates/iframeModal';
import './admin/apps.html';
import './admin/apps';
import './admin/appInstall.html';

@ -14,7 +14,7 @@ export class AppApisBridge {
this.appRouters = new Map();
// apiServer.use('/api/apps', (req, res, next) => {
// console.log({
// this.orch.debugLog({
// method: req.method.toLowerCase(),
// url: req.url,
// query: req.query,
@ -50,7 +50,7 @@ export class AppApisBridge {
}
registerApi({ api, computedPath, endpoint }, appId) {
console.log(`The App ${ appId } is registering the api: "${ endpoint.path }" (${ computedPath })`);
this.orch.debugLog(`The App ${ appId } is registering the api: "${ endpoint.path }" (${ computedPath })`);
this._verifyApi(api, endpoint);
@ -71,7 +71,7 @@ export class AppApisBridge {
}
unregisterApis(appId) {
console.log(`The App ${ appId } is unregistering all apis`);
this.orch.debugLog(`The App ${ appId } is unregistering all apis`);
if (this.appRouters.get(appId)) {
this.appRouters.delete(appId);

@ -10,7 +10,7 @@ export class AppCommandsBridge {
}
doesCommandExist(command, appId) {
console.log(`The App ${ appId } is checking if "${ command }" command exists.`);
this.orch.debugLog(`The App ${ appId } is checking if "${ command }" command exists.`);
if (typeof command !== 'string' || command.length === 0) {
return false;
@ -21,7 +21,7 @@ export class AppCommandsBridge {
}
enableCommand(command, appId) {
console.log(`The App ${ appId } is attempting to enable the command: "${ command }"`);
this.orch.debugLog(`The App ${ appId } is attempting to enable the command: "${ command }"`);
if (typeof command !== 'string' || command.trim().length === 0) {
throw new Error('Invalid command parameter provided, must be a string.');
@ -39,7 +39,7 @@ export class AppCommandsBridge {
}
disableCommand(command, appId) {
console.log(`The App ${ appId } is attempting to disable the command: "${ command }"`);
this.orch.debugLog(`The App ${ appId } is attempting to disable the command: "${ command }"`);
if (typeof command !== 'string' || command.trim().length === 0) {
throw new Error('Invalid command parameter provided, must be a string.');
@ -63,7 +63,7 @@ export class AppCommandsBridge {
// command: { command, paramsExample, i18nDescription, executor: function }
modifyCommand(command, appId) {
console.log(`The App ${ appId } is attempting to modify the command: "${ command }"`);
this.orch.debugLog(`The App ${ appId } is attempting to modify the command: "${ command }"`);
this._verifyCommand(command);
@ -85,7 +85,7 @@ export class AppCommandsBridge {
}
registerCommand(command, appId) {
console.log(`The App ${ appId } is registering the command: "${ command.command }"`);
this.orch.debugLog(`The App ${ appId } is registering the command: "${ command.command }"`);
this._verifyCommand(command);
@ -104,7 +104,7 @@ export class AppCommandsBridge {
}
unregisterCommand(command, appId) {
console.log(`The App ${ appId } is unregistering the command: "${ command }"`);
this.orch.debugLog(`The App ${ appId } is unregistering the command: "${ command }"`);
if (typeof command !== 'string' || command.trim().length === 0) {
throw new Error('Invalid command parameter provided, must be a string.');

@ -5,7 +5,7 @@ export class AppEnvironmentalVariableBridge {
}
async getValueByName(envVarName, appId) {
console.log(`The App ${ appId } is getting the environmental variable value ${ envVarName }.`);
this.orch.debugLog(`The App ${ appId } is getting the environmental variable value ${ envVarName }.`);
if (!(await this.isReadable(envVarName, appId))) {
throw new Error(`The environmental variable "${ envVarName }" is not readable.`);
@ -15,13 +15,13 @@ export class AppEnvironmentalVariableBridge {
}
async isReadable(envVarName, appId) {
console.log(`The App ${ appId } is checking if the environmental variable is readable ${ envVarName }.`);
this.orch.debugLog(`The App ${ appId } is checking if the environmental variable is readable ${ envVarName }.`);
return this.allowed.includes(envVarName.toUpperCase());
}
async isSet(envVarName, appId) {
console.log(`The App ${ appId } is checking if the environmental variable is set ${ envVarName }.`);
this.orch.debugLog(`The App ${ appId } is checking if the environmental variable is set ${ envVarName }.`);
if (!(await this.isReadable(envVarName, appId))) {
throw new Error(`The environmental variable "${ envVarName }" is not readable.`);

@ -6,7 +6,7 @@ export class AppHttpBridge {
info.request.content = JSON.stringify(info.request.data);
}
console.log(`The App ${ info.appId } is requesting from the outter webs:`, info);
this.orch.debugLog(`The App ${ info.appId } is requesting from the outter webs:`, info);
try {
return HTTP.call(info.method, info.url, info.request);

@ -15,8 +15,8 @@ export class AppListenerBridge {
// try {
// } catch (e) {
// console.log(`${ e.name }: ${ e.message }`);
// console.log(e.stack);
// this.orch.debugLog(`${ e.name }: ${ e.message }`);
// this.orch.debugLog(e.stack);
// }
}
@ -32,8 +32,8 @@ export class AppListenerBridge {
// try {
// } catch (e) {
// console.log(`${ e.name }: ${ e.message }`);
// console.log(e.stack);
// this.orch.debugLog(`${ e.name }: ${ e.message }`);
// this.orch.debugLog(e.stack);
// }
}
}

@ -10,7 +10,7 @@ export class AppMessageBridge {
}
async create(message, appId) {
console.log(`The App ${ appId } is creating a new message.`);
this.orch.debugLog(`The App ${ appId } is creating a new message.`);
let msg = this.orch.getConverters().get('messages').convertAppMessage(message);
@ -22,13 +22,17 @@ export class AppMessageBridge {
}
async getById(messageId, appId) {
console.log(`The App ${ appId } is getting the message: "${ messageId }"`);
this.orch.debugLog(`The App ${ appId } is getting the message: "${ messageId }"`);
return this.orch.getConverters().get('messages').convertById(messageId);
}
async update(message, appId) {
console.log(`The App ${ appId } is updating a message.`);
this.orch.debugLog(`The App ${ appId } is updating a message.`);
if (!this.updateMessage) {
const { updateMessage } = await import('meteor/rocketchat:lib');
this.updateMessage = updateMessage;
}
if (!message.editor) {
throw new Error('Invalid editor assigned to the message for the update.');
@ -45,7 +49,7 @@ export class AppMessageBridge {
}
async notifyUser(user, message, appId) {
console.log(`The App ${ appId } is notifying a user.`);
this.orch.debugLog(`The App ${ appId } is notifying a user.`);
const msg = this.orch.getConverters().get('messages').convertAppMessage(message);
@ -58,7 +62,7 @@ export class AppMessageBridge {
}
async notifyRoom(room, message, appId) {
console.log(`The App ${ appId } is notifying a room's users.`);
this.orch.debugLog(`The App ${ appId } is notifying a room's users.`);
if (room) {
const msg = this.orch.getConverters().get('messages').convertAppMessage(message);

@ -4,13 +4,13 @@ export class AppPersistenceBridge {
}
async purge(appId) {
console.log(`The App's persistent storage is being purged: ${ appId }`);
this.orch.debugLog(`The App's persistent storage is being purged: ${ appId }`);
this.orch.getPersistenceModel().remove({ appId });
}
async create(data, appId) {
console.log(`The App ${ appId } is storing a new object in their persistence.`, data);
this.orch.debugLog(`The App ${ appId } is storing a new object in their persistence.`, data);
if (typeof data !== 'object') {
throw new Error('Attempted to store an invalid data type, it must be an object.');
@ -20,7 +20,7 @@ export class AppPersistenceBridge {
}
async createWithAssociations(data, associations, appId) {
console.log(`The App ${ appId } is storing a new object in their persistence that is associated with some models.`, data, associations);
this.orch.debugLog(`The App ${ appId } is storing a new object in their persistence that is associated with some models.`, data, associations);
if (typeof data !== 'object') {
throw new Error('Attempted to store an invalid data type, it must be an object.');
@ -30,7 +30,7 @@ export class AppPersistenceBridge {
}
async readById(id, appId) {
console.log(`The App ${ appId } is reading their data in their persistence with the id: "${ id }"`);
this.orch.debugLog(`The App ${ appId } is reading their data in their persistence with the id: "${ id }"`);
const record = this.orch.getPersistenceModel().findOneById(id);
@ -38,7 +38,7 @@ export class AppPersistenceBridge {
}
async readByAssociations(associations, appId) {
console.log(`The App ${ appId } is searching for records that are associated with the following:`, associations);
this.orch.debugLog(`The App ${ appId } is searching for records that are associated with the following:`, associations);
const records = this.orch.getPersistenceModel().find({
appId,
@ -49,7 +49,7 @@ export class AppPersistenceBridge {
}
async remove(id, appId) {
console.log(`The App ${ appId } is removing one of their records by the id: "${ id }"`);
this.orch.debugLog(`The App ${ appId } is removing one of their records by the id: "${ id }"`);
const record = this.orch.getPersistenceModel().findOne({ _id: id, appId });
@ -63,7 +63,7 @@ export class AppPersistenceBridge {
}
async removeByAssociations(associations, appId) {
console.log(`The App ${ appId } is removing records with the following associations:`, associations);
this.orch.debugLog(`The App ${ appId } is removing records with the following associations:`, associations);
const query = {
appId,
@ -84,7 +84,7 @@ export class AppPersistenceBridge {
}
async update(id, data, upsert, appId) {
console.log(`The App ${ appId } is updating the record "${ id }" to:`, data);
this.orch.debugLog(`The App ${ appId } is updating the record "${ id }" to:`, data);
if (typeof data !== 'object') {
throw new Error('Attempted to store an invalid data type, it must be an object.');
@ -94,7 +94,7 @@ export class AppPersistenceBridge {
}
async updateByAssociations(associations, data, upsert, appId) {
console.log(`The App ${ appId } is updating the record with association to data as follows:`, associations, data);
this.orch.debugLog(`The App ${ appId } is updating the record with association to data as follows:`, associations, data);
if (typeof data !== 'object') {
throw new Error('Attempted to store an invalid data type, it must be an object.');

@ -9,7 +9,7 @@ export class AppRoomBridge {
}
async create(room, members, appId) {
console.log(`The App ${ appId } is creating a new room.`, room);
this.orch.debugLog(`The App ${ appId } is creating a new room.`, room);
const rcRoom = this.orch.getConverters().get('rooms').convertAppRoom(room);
let method;
@ -49,19 +49,19 @@ export class AppRoomBridge {
}
async getById(roomId, appId) {
console.log(`The App ${ appId } is getting the roomById: "${ roomId }"`);
this.orch.debugLog(`The App ${ appId } is getting the roomById: "${ roomId }"`);
return this.orch.getConverters().get('rooms').convertById(roomId);
}
async getByName(roomName, appId) {
console.log(`The App ${ appId } is getting the roomByName: "${ roomName }"`);
this.orch.debugLog(`The App ${ appId } is getting the roomByName: "${ roomName }"`);
return this.orch.getConverters().get('rooms').convertByName(roomName);
}
async getCreatorById(roomId, appId) {
console.log(`The App ${ appId } is getting the room's creator by id: "${ roomId }"`);
this.orch.debugLog(`The App ${ appId } is getting the room's creator by id: "${ roomId }"`);
const room = Rooms.findOneById(roomId);
@ -73,7 +73,7 @@ export class AppRoomBridge {
}
async getCreatorByName(roomName, appId) {
console.log(`The App ${ appId } is getting the room's creator by name: "${ roomName }"`);
this.orch.debugLog(`The App ${ appId } is getting the room's creator by name: "${ roomName }"`);
const room = Rooms.findOneByName(roomName);
@ -85,13 +85,13 @@ export class AppRoomBridge {
}
async getMembers(roomId, appId) {
console.log(`The App ${ appId } is getting the room's members by room id: "${ roomId }"`);
this.orch.debugLog(`The App ${ appId } is getting the room's members by room id: "${ roomId }"`);
const subscriptions = await Subscriptions.findByRoomId(roomId);
return subscriptions.map((sub) => this.orch.getConverters().get('users').convertById(sub.u && sub.u._id));
}
async getDirectByUsernames(usernames, appId) {
console.log(`The App ${ appId } is getting direct room by usernames: "${ usernames }"`);
this.orch.debugLog(`The App ${ appId } is getting direct room by usernames: "${ usernames }"`);
const room = await Rooms.findDirectRoomContainingAllUsernames(usernames);
if (!room) {
return undefined;
@ -100,7 +100,7 @@ export class AppRoomBridge {
}
async update(room, members = [], appId) {
console.log(`The App ${ appId } is updating a room.`);
this.orch.debugLog(`The App ${ appId } is updating a room.`);
if (!room.id || !Rooms.findOneById(room.id)) {
throw new Error('A room must exist to update.');

@ -22,7 +22,7 @@ export class AppSettingBridge {
}
async getAll(appId) {
console.log(`The App ${ appId } is getting all the settings.`);
this.orch.debugLog(`The App ${ appId } is getting all the settings.`);
return Settings.find({ _id: { $nin: this.disallowedSettings } })
.fetch()
@ -30,7 +30,7 @@ export class AppSettingBridge {
}
async getOneById(id, appId) {
console.log(`The App ${ appId } is getting the setting by id ${ id }.`);
this.orch.debugLog(`The App ${ appId } is getting the setting by id ${ id }.`);
if (!this.isReadableById(id, appId)) {
throw new Error(`The setting "${ id }" is not readable.`);
@ -40,13 +40,13 @@ export class AppSettingBridge {
}
async hideGroup(name, appId) {
console.log(`The App ${ appId } is hidding the group ${ name }.`);
this.orch.debugLog(`The App ${ appId } is hidding the group ${ name }.`);
throw new Error('Method not implemented.');
}
async hideSetting(id, appId) {
console.log(`The App ${ appId } is hidding the setting ${ id }.`);
this.orch.debugLog(`The App ${ appId } is hidding the setting ${ id }.`);
if (!this.isReadableById(id, appId)) {
throw new Error(`The setting "${ id }" is not readable.`);
@ -56,13 +56,13 @@ export class AppSettingBridge {
}
async isReadableById(id, appId) {
console.log(`The App ${ appId } is checking if they can read the setting ${ id }.`);
this.orch.debugLog(`The App ${ appId } is checking if they can read the setting ${ id }.`);
return !this.disallowedSettings.includes(id);
}
async updateOne(setting, appId) {
console.log(`The App ${ appId } is updating the setting ${ setting.id } .`);
this.orch.debugLog(`The App ${ appId } is updating the setting ${ setting.id } .`);
if (!this.isReadableById(setting.id, appId)) {
throw new Error(`The setting "${ setting.id }" is not readable.`);

@ -4,13 +4,13 @@ export class AppUserBridge {
}
async getById(userId, appId) {
console.log(`The App ${ appId } is getting the userId: "${ userId }"`);
this.orch.debugLog(`The App ${ appId } is getting the userId: "${ userId }"`);
return this.orch.getConverters().get('users').convertById(userId);
}
async getByUsername(username, appId) {
console.log(`The App ${ appId } is getting the username: "${ username }"`);
this.orch.debugLog(`The App ${ appId } is getting the username: "${ username }"`);
return this.orch.getConverters().get('users').convertByUsername(username);
}

@ -1,8 +1,12 @@
import { Meteor } from 'meteor/meteor';
import { HTTP } from 'meteor/http';
import { API } from '../../../api/server/api';
import { API } from '../../../api/server';
import Busboy from 'busboy';
import { getWorkspaceAccessToken } from '../../../cloud/server';
import { settings } from '../../../settings';
import { Info } from '../../../utils';
export class AppsRestApi {
constructor(orch, manager) {
this._orch = orch;
@ -49,6 +53,50 @@ export class AppsRestApi {
this.api.addRoute('', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
const baseUrl = settings.get('Apps_Framework_Marketplace_Url');
// Gets the Apps from the marketplace
if (this.queryParams.marketplace) {
const headers = {};
const token = getWorkspaceAccessToken();
if (token) {
headers.Authorization = `Bearer ${ token }`;
}
const result = HTTP.get(`${ baseUrl }/v1/apps?version=${ Info.marketplaceApiVersion }`, {
headers,
});
if (result.statusCode !== 200) {
return API.v1.failure();
}
return API.v1.success(result.data);
}
if (this.queryParams.categories) {
const headers = {};
const token = getWorkspaceAccessToken();
if (token) {
headers.Authorization = `Bearer ${ token }`;
}
const result = HTTP.get(`${ baseUrl }/v1/categories`, {
headers,
});
if (result.statusCode !== 200) {
return API.v1.failure();
}
return API.v1.success(result.data);
}
if (this.queryParams.buildBuyUrl && this.queryParams.appId) {
const workspaceId = settings.get('Cloud_Workspace_Id');
return API.v1.success({ url: `${ baseUrl }/apps/${ this.queryParams.appId }/buy?workspaceId=${ workspaceId }` });
}
const apps = manager.get().map((prl) => {
const info = prl.getInfo();
info.languages = prl.getStorageItem().languageContent;
@ -63,14 +111,42 @@ export class AppsRestApi {
let buff;
if (this.bodyParams.url) {
const result = HTTP.call('GET', this.bodyParams.url, { npmRequestOptions: { encoding: 'base64' } });
if (settings.get('Apps_Framework_Development_Mode') !== true) {
return API.v1.failure({ error: 'Installation from url is disabled.' });
}
const result = HTTP.call('GET', this.bodyParams.url, { npmRequestOptions: { encoding: 'binary' } });
if (result.statusCode !== 200 || !result.headers['content-type'] || result.headers['content-type'] !== 'application/zip') {
return API.v1.failure({ error: 'Invalid url. It doesn\'t exist or is not "application/zip".' });
}
buff = Buffer.from(result.content, 'base64');
buff = Buffer.from(result.content, 'binary');
} else if (this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) {
const baseUrl = settings.get('Apps_Framework_Marketplace_Url');
const headers = {};
const token = getWorkspaceAccessToken(true, 'marketplace:download', false);
const result = HTTP.get(`${ baseUrl }/v1/apps/${ this.bodyParams.appId }/download/${ this.bodyParams.version }?token=${ token }`, {
headers,
npmRequestOptions: { encoding: 'binary' },
});
if (result.statusCode !== 200) {
return API.v1.failure();
}
if (!result.headers['content-type'] || result.headers['content-type'] !== 'application/zip') {
return API.v1.failure({ error: 'Invalid url. It doesn\'t exist or is not "application/zip".' });
}
buff = Buffer.from(result.content, 'binary');
} else {
if (settings.get('Apps_Framework_Development_Mode') !== true) {
return API.v1.failure({ error: 'Direct installation of an App is disabled.' });
}
buff = fileHandler(this.request, 'app');
}
@ -109,7 +185,46 @@ export class AppsRestApi {
this.api.addRoute(':id', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
console.log('Getting:', this.urlParams.id);
if (this.queryParams.marketplace && this.queryParams.version) {
const baseUrl = settings.get('Apps_Framework_Marketplace_Url');
const headers = {};
const token = getWorkspaceAccessToken();
if (token) {
headers.Authorization = `Bearer ${ token }`;
}
const result = HTTP.get(`${ baseUrl }/v1/apps/${ this.urlParams.id }?appVersion=${ this.queryParams.version }`, {
headers,
});
if (result.statusCode !== 200 || result.data.length === 0) {
return API.v1.failure();
}
return API.v1.success({ app: result.data[0] });
}
if (this.queryParams.marketplace && this.queryParams.update && this.queryParams.appVersion) {
const baseUrl = settings.get('Apps_Framework_Marketplace_Url');
const headers = {};
const token = getWorkspaceAccessToken();
if (token) {
headers.Authorization = `Bearer ${ token }`;
}
const result = HTTP.get(`${ baseUrl }/v1/apps/${ this.urlParams.id }/latest?frameworkVersion=${ Info.marketplaceApiVersion }`, {
headers,
});
if (result.statusCode !== 200 || result.data.length === 0) {
return API.v1.failure();
}
return API.v1.success({ app: result.data });
}
const prl = manager.getOneById(this.urlParams.id);
if (prl) {
@ -122,20 +237,50 @@ export class AppsRestApi {
}
},
post() {
console.log('Updating:', this.urlParams.id);
// TODO: Verify permissions
let buff;
if (this.bodyParams.url) {
const result = HTTP.call('GET', this.bodyParams.url, { npmRequestOptions: { encoding: 'base64' } });
if (settings.get('Apps_Framework_Development_Mode') !== true) {
return API.v1.failure({ error: 'Updating an App from a url is disabled.' });
}
const result = HTTP.call('GET', this.bodyParams.url, { npmRequestOptions: { encoding: 'binary' } });
if (result.statusCode !== 200 || !result.headers['content-type'] || result.headers['content-type'] !== 'application/zip') {
return API.v1.failure({ error: 'Invalid url. It doesn\'t exist or is not "application/zip".' });
}
buff = Buffer.from(result.content, 'base64');
buff = Buffer.from(result.content, 'binary');
} else if (this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) {
const baseUrl = settings.get('Apps_Framework_Marketplace_Url');
const headers = {};
const token = getWorkspaceAccessToken();
if (token) {
headers.Authorization = `Bearer ${ token }`;
}
const result = HTTP.get(`${ baseUrl }/v1/apps/${ this.bodyParams.appId }/download/${ this.bodyParams.version }`, {
headers,
npmRequestOptions: { encoding: 'binary' },
});
if (result.statusCode !== 200) {
return API.v1.failure();
}
if (!result.headers['content-type'] || result.headers['content-type'] !== 'application/zip') {
return API.v1.failure({ error: 'Invalid url. It doesn\'t exist or is not "application/zip".' });
}
buff = Buffer.from(result.content, 'binary');
} else {
if (settings.get('Apps_Framework_Development_Mode') !== true) {
return API.v1.failure({ error: 'Direct updating of an App is disabled.' });
}
buff = fileHandler(this.request, 'app');
}
@ -160,7 +305,6 @@ export class AppsRestApi {
});
},
delete() {
console.log('Uninstalling:', this.urlParams.id);
const prl = manager.getOneById(this.urlParams.id);
if (prl) {
@ -178,7 +322,6 @@ export class AppsRestApi {
this.api.addRoute(':id/icon', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
console.log('Getting the App\'s Icon:', this.urlParams.id);
const prl = manager.getOneById(this.urlParams.id);
if (prl) {
@ -193,7 +336,6 @@ export class AppsRestApi {
this.api.addRoute(':id/languages', { authRequired: false }, {
get() {
console.log(`Getting ${ this.urlParams.id }'s languages..`);
const prl = manager.getOneById(this.urlParams.id);
if (prl) {
@ -208,7 +350,6 @@ export class AppsRestApi {
this.api.addRoute(':id/logs', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
console.log(`Getting ${ this.urlParams.id }'s logs..`);
const prl = manager.getOneById(this.urlParams.id);
if (prl) {
@ -234,7 +375,6 @@ export class AppsRestApi {
this.api.addRoute(':id/settings', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
console.log(`Getting ${ this.urlParams.id }'s settings..`);
const prl = manager.getOneById(this.urlParams.id);
if (prl) {
@ -252,7 +392,6 @@ export class AppsRestApi {
}
},
post() {
console.log(`Updating ${ this.urlParams.id }'s settings..`);
if (!this.bodyParams || !this.bodyParams.settings) {
return API.v1.failure('The settings to update must be present.');
}
@ -280,8 +419,6 @@ export class AppsRestApi {
this.api.addRoute(':id/settings/:settingId', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
console.log(`Getting the App ${ this.urlParams.id }'s setting ${ this.urlParams.settingId }`);
try {
const setting = manager.getSettingsManager().getAppSetting(this.urlParams.id, this.urlParams.settingId);
@ -297,8 +434,6 @@ export class AppsRestApi {
}
},
post() {
console.log(`Updating the App ${ this.urlParams.id }'s setting ${ this.urlParams.settingId }`);
if (!this.bodyParams.setting) {
return API.v1.failure('Setting to update to must be present on the posted body.');
}
@ -321,7 +456,6 @@ export class AppsRestApi {
this.api.addRoute(':id/apis', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
console.log(`Getting ${ this.urlParams.id }'s apis..`);
const prl = manager.getOneById(this.urlParams.id);
if (prl) {
@ -336,7 +470,6 @@ export class AppsRestApi {
this.api.addRoute(':id/status', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
console.log(`Getting ${ this.urlParams.id }'s status..`);
const prl = manager.getOneById(this.urlParams.id);
if (prl) {
@ -350,7 +483,6 @@ export class AppsRestApi {
return API.v1.failure('Invalid status provided, it must be "status" field and a string.');
}
console.log(`Updating ${ this.urlParams.id }'s status...`, this.bodyParams.status);
const prl = manager.getOneById(this.urlParams.id);
if (prl) {

@ -15,6 +15,8 @@ class AppServerOrchestrator {
Permissions.createOrUpdate('manage-apps', ['admin']);
}
this._inDebug = process.env.NODE_ENV !== 'production';
this._model = new AppsModel();
this._logModel = new AppsLogsModel();
this._persistModel = new AppsPersistenceModel();
@ -77,6 +79,17 @@ class AppServerOrchestrator {
return this.getManager().areAppsLoaded();
}
isDebugging() {
return this._inDebug;
}
debugLog() {
if (this._inDebug) {
// eslint-disable-next-line
console.log(...arguments);
}
}
load() {
// Don't try to load it again if it has
// already been loaded
@ -108,6 +121,21 @@ settings.addGroup('General', function() {
type: 'boolean',
hidden: false,
});
this.add('Apps_Framework_Development_Mode', false, {
type: 'boolean',
enableQuery: {
_id: 'Apps_Framework_enabled',
value: true,
},
public: true,
hidden: false,
});
this.add('Apps_Framework_Marketplace_Url', 'https://marketplace.rocket.chat', {
type: 'string',
hidden: true,
});
});
});

@ -6,7 +6,7 @@ import { Settings } from '../../../models';
import { getRedirectUri } from './getRedirectUri';
export function getWorkspaceAccessToken() {
export function getWorkspaceAccessToken(forceNew = false, scope = '', save = true) {
if (!settings.get('Register_Server')) {
return '';
}
@ -19,7 +19,7 @@ export function getWorkspaceAccessToken() {
const expires = Settings.findOneById('Cloud_Workspace_Access_Token_Expires_At');
const now = new Date();
if (now < expires.value) {
if (now < expires.value && !forceNew) {
return settings.get('Cloud_Workspace_Access_Token');
}
@ -34,6 +34,7 @@ export function getWorkspaceAccessToken() {
query: querystring.stringify({
client_id,
client_secret,
scope,
grant_type: 'client_credentials',
redirect_uri: redirectUri,
}),
@ -42,12 +43,13 @@ export function getWorkspaceAccessToken() {
return '';
}
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + authTokenResult.data.expires_in);
Settings.updateValueById('Cloud_Workspace_Access_Token', authTokenResult.data.access_token);
Settings.updateValueById('Cloud_Workspace_Access_Token_Expires_At', expiresAt);
if (save) {
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + authTokenResult.data.expires_in);
Settings.updateValueById('Cloud_Workspace_Access_Token', authTokenResult.data.access_token);
Settings.updateValueById('Cloud_Workspace_Access_Token_Expires_At', expiresAt);
}
return authTokenResult.data.access_token;
}

@ -23,11 +23,6 @@
}
&-search {
height: 48px;
margin-right: 5px;
margin-left: 5px;
& .rc-icon {
width: 0.875rem;
}

@ -6,7 +6,11 @@
{{> burger}}
</div>
<span class="rc-header__block">{{_ sectionName}}</span>
{{#if rawSectionName}}
<span class="rc-header__block">{{rawSectionName}}</span>
{{else}}
<span class="rc-header__block">{{_ sectionName}}</span>
{{/if}}
{{#if Template.contentBlock}}
{{> Template.contentBlock}}

@ -343,6 +343,8 @@
"Apply_and_refresh_all_clients": "Apply and refresh all clients",
"Apps": "Apps",
"Apps_Engine_Version": "Apps Engine Version",
"Apps_Framework_Development_Mode": "Eneble development mode",
"Apps_Framework_Development_Mode_Description": "Development mode allows the installation of Apps that are not from the Rocket.Chat's Marketplace.",
"Apps_Framework_enabled": "Enable the App Framework",
"Apps_Settings": "App's Settings",
"Apps_WhatIsIt": "Apps: What Are They?",
@ -449,6 +451,7 @@
"Broadcasting_client_secret": "Broadcasting Client Secret",
"Broadcasting_enabled": "Broadcasting Enabled",
"Broadcasting_media_server_url": "Broadcasting Media Server URL",
"Browse_Files": "Browse Files",
"Bugsnag_api_key": "Bugsnag API Key",
"Build_Environment": "Build Environment",
"bulk-create-c": "Bulk Create Channels",
@ -1391,6 +1394,7 @@
"Forward_chat": "Forward chat",
"Forward_to_department": "Forward to department",
"Forward_to_user": "Forward to user",
"Free": "Free",
"Frequently_Used": "Frequently Used",
"Friday": "Friday",
"From": "From",
@ -1543,6 +1547,7 @@
"Install_FxOs_follow_instructions": "Please confirm the app installation on your device (press \"Install\" when prompted).",
"Install_package": "Install package",
"Installation": "Installation",
"Installed": "Installed",
"Installed_at": "Installed at",
"Invitation_HTML": "Invitation HTML",
"Instance_Record": "Instance Record",
@ -2292,6 +2297,7 @@
"Public_Channel": "Public Channel",
"Public_Community": "Public Community",
"Public_Relations": "Public Relations",
"Purchased": "Purchased",
"Push": "Push",
"Push_Setting_Requires_Restart_Alert": "Changing this value requires restarting Rocket.Chat.",
"Push_apn_cert": "APN Cert",
@ -2310,6 +2316,8 @@
"Push_show_message": "Show Message in Notification",
"Push_show_username_room": "Show Channel/Group/Username in Notification",
"Push_test_push": "Test",
"Purchase_for_free": "Purchase for FREE",
"Purchase_for_price": "Purchase for $%s",
"Query": "Query",
"Query_description": "Additional conditions for determining which users to send the email to. Unsubscribed users are automatically removed from the query. It must be a valid JSON. Example: \"{\"createdAt\":{\"$gt\":{\"$date\": \"2015-01-01T00:00:00.000Z\"}}}\"",
"Queue": "Queue",
@ -3169,4 +3177,4 @@
"Your_question": "Your question",
"Your_server_link": "Your server link",
"Your_workspace_is_ready": "Your workspace is ready to use 🎉"
}
}

Loading…
Cancel
Save