Skip to content
Merged
72 changes: 23 additions & 49 deletions apps/meteor/ee/server/apps/communication/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type express from 'express';
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';

import { actionButtonsHandler } from './endpoints/actionButtonsHandler';
import { appsCountHandler } from './endpoints/appsCountHandler';
import type { APIClass } from '../../../../app/api/server';
import { API } from '../../../../app/api/server';
import { getPaginationItems } from '../../../../app/api/server/helpers/getPaginationItems';
Expand All @@ -23,10 +25,10 @@ import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmin
import { canEnableApp } from '../../../app/license/server/canEnableApp';
import { formatAppInstanceForRest } from '../../../lib/misc/formatAppInstanceForRest';
import { notifyAppInstall } from '../marketplace/appInstall';
import { fetchMarketplaceApps } from '../marketplace/fetchMarketplaceApps';
import { MarketplaceConnectionError, MarketplaceAppsError } from '../marketplace/marketplaceErrors';
import type { AppServerOrchestrator } from '../orchestrator';
import { Apps } from '../orchestrator';
import { actionButtonsHandler } from './endpoints/actionButtonsHandler';
import { appsCountHandler } from './endpoints/appsCountHandler';

const rocketChatVersion = Info.version;
const appsEngineVersionForMarketplace = Info.marketplaceApiVersion.replace(/-.*/g, '');
Expand Down Expand Up @@ -110,43 +112,20 @@ export class AppsRestApi {
{ authRequired: true },
{
async get() {
const baseUrl = orchestrator.getMarketplaceUrl();

// Gets the Apps from the marketplace
const headers = getDefaultHeaders();
const token = await getWorkspaceAccessToken();
if (token) {
headers.Authorization = `Bearer ${token}`;
}

let result;
try {
const request = await fetch(`${baseUrl}/v1/apps`, {
headers,
params: {
...(this.queryParams.isAdminUser === 'false' && { endUserID: this.user._id }),
},
});

if (request.status === 426) {
orchestrator.getRocketChatLogger().error('Workspace out of support window:', await request.json());
return API.v1.failure({ error: 'unsupported version' });
const apps = await fetchMarketplaceApps({ ...(this.queryParams.isAdminUser === 'false' && { endUserID: this.user._id }) });
return API.v1.success(apps);
} catch (err) {
if (err instanceof MarketplaceConnectionError) {
return handleError('Unable to access Marketplace. Does the server has access to the internet?', err);
}

if (request.status !== 200) {
orchestrator.getRocketChatLogger().error('Error getting the Apps:', await request.json());
return API.v1.failure();
if (err instanceof MarketplaceAppsError) {
return API.v1.failure({ error: err.message });
}
result = await request.json();

if (!request.ok) {
throw new Error(result.error);
}
} catch (e) {
return handleError('Unable to access Marketplace. Does the server has access to the internet?', e);
return API.v1.internalError();
}

return API.v1.success(result);
},
},
);
Expand Down Expand Up @@ -236,25 +215,20 @@ export class AppsRestApi {
if ('marketplace' in this.queryParams && this.queryParams.marketplace) {
apiDeprecationLogger.endpoint(this.request.route, '7.0.0', this.response, 'Use /apps/marketplace to get the apps list.');

const headers = getDefaultHeaders();
const token = await getWorkspaceAccessToken();
if (token) {
headers.Authorization = `Bearer ${token}`;
}

let result;
try {
const request = await fetch(`${baseUrl}/v1/apps`, { headers });
if (request.status !== 200) {
orchestrator.getRocketChatLogger().error('Error getting the Apps:', await request.json());
return API.v1.failure();
}
result = await request.json();
const apps = await fetchMarketplaceApps();
return API.v1.success(apps);
} catch (e) {
return handleError('Unable to access Marketplace. Does the server has access to the internet?', e);
}
if (e instanceof MarketplaceConnectionError) {
return handleError('Unable to access Marketplace. Does the server has access to the internet?', e);
}

return API.v1.success(result);
if (e instanceof MarketplaceAppsError) {
return API.v1.failure({ error: e.message });
}

return API.v1.internalError();
}
}

if ('categories' in this.queryParams && this.queryParams.categories) {
Expand Down
180 changes: 180 additions & 0 deletions apps/meteor/ee/server/apps/marketplace/fetchMarketplaceApps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import type { App } from '@rocket.chat/core-typings';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import { v, compile } from 'suretype';

import { getMarketplaceHeaders } from './getMarketplaceHeaders';
import { getWorkspaceAccessToken } from '../../../../app/cloud/server';
import { Apps } from '../orchestrator';
import { MarketplaceAppsError, MarketplaceConnectionError } from './marketplaceErrors';

type FetchMarketplaceAppsParams = {
endUserID?: string;
};

const markdownObject = {
raw: v.string(),
rendered: v.string(),
};

const fetchMarketplaceAppsSchema = v.array(
v.object({
appId: v.string().required(),
latest: v
.object({
internalId: v.string(),
id: v.string().required(),
name: v.string().required(),
nameSlug: v.string().required(),
version: v.string().required(),
categories: v.array(v.string()).required(),
languages: v.array(v.string()),
shortDescription: v.string(),
description: v.string().required(),
privacyPolicySummary: v.string(),
documentationUrl: v.string(),
detailedDescription: v.object(markdownObject).required(),
detailedChangelog: v.object(markdownObject).required(),

requiredApiVersion: v.string().required(),
versionIncompatible: v.boolean(),

permissions: v.array(
v.object({
name: v.string().required(),
domains: v.array(v.string()),
scopes: v.array(v.string()),
}),
),
addon: v.string(),
author: v
.object({
name: v.string().required(),
support: v.string().required(),
homepage: v.string().required(),
})
.required(),
classFile: v.string().required(),
iconFile: v.string().required(),
iconFileData: v.string().required(),
status: v.string().enum('submitted', 'author-rejected', 'author-approved', 'rejected', 'approved').required(),
reviewedNote: v.string(),
rejectionNote: v.string(),
changesNote: v.string(),
internalChangesNote: v.string(),
isVisible: v.boolean().required(),
createdDate: v.string().required(),
modifiedDate: v.string().required(),
})
.required(),
isAddon: v.boolean().required(),
addonId: v.string(),
isEnterpriseOnly: v.boolean().required(),
isBundle: v.boolean().required(),
bundedAppIds: v.array(v.string()).required(),
bundledIn: v
.array(
v.object({
bundleId: v.string(),
bundleName: v.string(),
addonTierId: v.string(),
}),
)
.required(),
isPurchased: v.boolean().required(),
isSubscribed: v.boolean().required(),
subscriptionInfo: v.object({
typeOf: v.string().enum('app', 'service').required(),
status: v.string().enum('trialing', 'active', 'cancelled', 'cancelling', 'pastDue').required(),
statusFromBilling: v.boolean().required(),
isSeatBased: v.boolean().required(),
seats: v.number().required(),
maxSeats: v.number().required(),
license: v
.object({
license: v.string().required(),
version: v.number().required(),
expireDate: v.string().required(),
})
.required(),
startDate: v.string().required(),
periodEnd: v.string().required(),
endDate: v.string(),
externallyManaged: v.boolean().required(),
isSubscribedViaBundle: v.boolean().required(),
}),
price: v.number().required(),
purchaseType: v.string().enum('', 'buy', 'subscription').required(),
pricingPlans: v.array(
v.object({
id: v.string().required(),
enabled: v.boolean().required(),
price: v.number().required(),
trialDays: v.number().required(),
strategy: v.string().enum('once', 'monthly', 'yearly').required(),
isPerSeat: v.boolean().required(),
tiers: v.array(
v.object({
perUnit: v.boolean().required(),
minimum: v.number().required(),
maximum: v.number().required(),
price: v.number().required(),
refId: v.string(),
}),
),
}),
),
isUsageBased: v.boolean().required(),

requestedEndUser: v.boolean(),
requested: v.boolean(),
appRequestStats: v.object({}),

createdAt: v.string().required(),
modifiedAt: v.string().required(),
}),
);

const assertMarketplaceAppsSchema = compile(fetchMarketplaceAppsSchema);

export async function fetchMarketplaceApps({ endUserID }: FetchMarketplaceAppsParams = {}): Promise<App[]> {
const baseUrl = Apps.getMarketplaceUrl();
const headers = getMarketplaceHeaders();
const token = await getWorkspaceAccessToken();
if (token) {
headers.Authorization = `Bearer ${token}`;
}

let request;
try {
request = await fetch(`${baseUrl}/v1/apps`, {
headers,
params: {
...(endUserID && { endUserID }),
},
});
} catch (error) {
throw new MarketplaceConnectionError('Marketplace_Bad_Marketplace_Connection');
}

if (request.status === 200) {
const response = await request.json();
assertMarketplaceAppsSchema(response);
return response;
}

const response = await request.json();

Apps.getRocketChatLogger().error('Failed to fetch marketplace apps', response);

if (request.status === 400 && response.code === 200) {
throw new MarketplaceAppsError('Marketplace_Invalid_Apps_Engine_Version');
}

const INTERNAL_MARKETPLACE_ERROR_CODES = [266, 256, 166, 221, 257, 320];

if (request.status === 500 && INTERNAL_MARKETPLACE_ERROR_CODES.includes(response.code)) {
throw new MarketplaceAppsError('Marketplace_Internal_Error');
}

throw new MarketplaceAppsError('Marketplace_Failed_To_Fetch_Apps');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Info } from '../../../../app/utils/rocketchat.info';

export function getMarketplaceHeaders(): Record<string, any> {
return {
'X-Apps-Engine-Version': Info.marketplaceApiVersion.replace(/-.*/g, ''),
};
}
13 changes: 13 additions & 0 deletions apps/meteor/ee/server/apps/marketplace/marketplaceErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class MarketplaceAppsError extends Error {
constructor(message: string) {
super(message);
this.name = 'MarketplaceAppsError';
}
}

export class MarketplaceConnectionError extends Error {
constructor(message: string) {
super(message);
this.name = 'MarketplaceConnectionError';
}
}
4 changes: 4 additions & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -3614,6 +3614,10 @@
"Marketplace_error": "Cannot connect to internet or your workspace may be an offline install.",
"Marketplace_unavailable": "Marketplace unavailable",
"Marketplace_unavailable_description": "This workspace cannot access the marketplace because it’s running an unsupported version of Rocket.Chat. Ask your workspace admin to update and regain access.",
"Marketplace_Bad_Marketplace_Connection": "Cannot connect to the marketplace. Please check your internet connection.",
"Marketplace_Invalid_Apps_Engine_Version": "The installed Apps Engine version is not compatible with the marketplace. Please update the Apps Engine to the latest version.",
"Marketplace_Internal_Error": "An internal error occurred communicating with Marketplace. Please try again later.",
"Marketplace_Failed_To_Fetch_Apps": "Failed to fetch apps from the marketplace. Please try again later.",
"MAU_value": "MAU {{value}}",
"Max_length_is": "Max length is %s",
"Max_number_incoming_livechats_displayed": "Max number of items displayed in the queue",
Expand Down
Loading