RocketChat/Rocket.Chat

View on GitHub
apps/meteor/ee/server/apps/communication/rest.ts

Summary

Maintainability
F
3 wks
Test Coverage
import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus';
import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager';
import { AppInstallationSource } from '@rocket.chat/apps-engine/server/storage';
import type { IUser, IMessage } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { Settings, Users } from '@rocket.chat/models';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import { Meteor } from 'meteor/meteor';

import type { APIClass } from '../../../../app/api/server';
import { API } from '../../../../app/api/server';
import { getPaginationItems } from '../../../../app/api/server/helpers/getPaginationItems';
import { getUploadFormData } from '../../../../app/api/server/lib/getUploadFormData';
import { getWorkspaceAccessToken, getWorkspaceAccessTokenWithScope } from '../../../../app/cloud/server';
import { apiDeprecationLogger } from '../../../../app/lib/server/lib/deprecationWarningLogger';
import { settings } from '../../../../app/settings/server';
import { Info } from '../../../../app/utils/rocketchat.info';
import { i18n } from '../../../../server/lib/i18n';
import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmins';
import { canEnableApp } from '../../../app/license/server/canEnableApp';
import { formatAppInstanceForRest } from '../../../lib/misc/formatAppInstanceForRest';
import { appEnableCheck } from '../marketplace/appEnableCheck';
import { notifyAppInstall } from '../marketplace/appInstall';
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, '');
const getDefaultHeaders = (): Record<string, any> => ({
    'X-Apps-Engine-Version': appsEngineVersionForMarketplace,
});

const purchaseTypes = new Set(['buy', 'subscription']);

export class AppsRestApi {
    public api: APIClass<'/apps'>;

    public _orch: AppServerOrchestrator;

    public _manager: AppManager;

    constructor(orch: AppServerOrchestrator, manager: AppManager) {
        this._orch = orch;
        this._manager = manager;
        void this.loadAPI();
    }

    async loadAPI() {
        this.api = new API.ApiClass({
            version: 'apps',
            useDefaultAuth: true,
            prettyJson: false,
            enableCors: false,
            auth: API.getUserAuth(),
        });
        this.addManagementRoutes();
    }

    addManagementRoutes() {
        const orchestrator = this._orch;
        const manager = this._manager;

        const handleError = (message: string, e: any) => {
            // when there is no `response` field in the error, it means the request
            // couldn't even make it to the server
            if (!e.hasOwnProperty('response')) {
                orchestrator.getRocketChatLogger().warn(message, e.message);
                return API.v1.internalError('Could not reach the Marketplace');
            }

            orchestrator.getRocketChatLogger().error(message, e.response.data);

            if (e.response.statusCode >= 500 && e.response.statusCode <= 599) {
                return API.v1.internalError();
            }

            if (e.response.statusCode === 404) {
                return API.v1.notFound();
            }

            return API.v1.failure();
        };

        this.api.addRoute('actionButtons', ...actionButtonsHandler(this));
        this.api.addRoute('count', ...appsCountHandler(this));

        this.api.addRoute(
            'incompatibleModal',
            { authRequired: true },
            {
                async get() {
                    const baseUrl = orchestrator.getMarketplaceUrl();
                    const workspaceId = settings.get('Cloud_Workspace_Id');
                    const { action, appId, appVersion } = this.queryParams;

                    return API.v1.success({
                        url: `${baseUrl}/apps/${appId}/incompatible/${appVersion}/${action}?workspaceId=${workspaceId}&rocketChatVersion=${rocketChatVersion}`,
                    });
                },
            },
        );

        this.api.addRoute(
            'marketplace',
            { 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 !== 200) {
                            orchestrator.getRocketChatLogger().error('Error getting the Apps:', await request.json());
                            return API.v1.failure();
                        }
                        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.success(result);
                },
            },
        );

        this.api.addRoute(
            'categories',
            { authRequired: true },
            {
                async get() {
                    const baseUrl = orchestrator.getMarketplaceUrl();

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

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

                    return API.v1.success(result);
                },
            },
        );

        this.api.addRoute(
            'buildExternalUrl',
            { authRequired: true },
            {
                async get() {
                    const baseUrl = orchestrator.getMarketplaceUrl();

                    const workspaceId = settings.get('Cloud_Workspace_Id');

                    if (!this.queryParams.purchaseType || !purchaseTypes.has(this.queryParams.purchaseType)) {
                        return API.v1.failure({ error: 'Invalid purchase type' });
                    }

                    const response = await getWorkspaceAccessTokenWithScope('marketplace:purchase');
                    if (!response.token) {
                        return API.v1.failure({ error: 'Unauthorized' });
                    }

                    const subscribeRoute = this.queryParams.details === 'true' ? 'subscribe/details' : 'subscribe';

                    const seats = await Users.getActiveLocalUserCount();

                    return API.v1.success({
                        url: `${baseUrl}/apps/${this.queryParams.appId}/${
                            this.queryParams.purchaseType === 'buy' ? this.queryParams.purchaseType : subscribeRoute
                        }?workspaceId=${workspaceId}&token=${response.token}&seats=${seats}`,
                    });
                },
            },
        );

        this.api.addRoute(
            'installed',
            { authRequired: true },
            {
                async get() {
                    const apps = manager.get().map(formatAppInstanceForRest);
                    return API.v1.success({ apps });
                },
            },
        );

        // WE NEED TO MOVE EACH ENDPOINT HANDLER TO IT'S OWN FILE
        this.api.addRoute(
            '',
            { authRequired: true, permissionsRequired: ['manage-apps'] },
            {
                async get() {
                    const baseUrl = orchestrator.getMarketplaceUrl();

                    // Gets the Apps from the marketplace
                    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();
                        } catch (e) {
                            return handleError('Unable to access Marketplace. Does the server has access to the internet?', e);
                        }

                        return API.v1.success(result);
                    }

                    if ('categories' in this.queryParams && this.queryParams.categories) {
                        apiDeprecationLogger.endpoint(this.request.route, '7.0.0', this.response, 'Use /apps/categories to get the categories list.');
                        const headers = getDefaultHeaders();
                        const token = await getWorkspaceAccessToken();
                        if (token) {
                            headers.Authorization = `Bearer ${token}`;
                        }

                        let result;
                        try {
                            const request = await fetch(`${baseUrl}/v1/categories`, { headers });
                            if (request.status !== 200) {
                                orchestrator.getRocketChatLogger().error('Error getting the Apps:', await request.json());
                                return API.v1.failure();
                            }
                            result = await request.json();
                        } catch (e: any) {
                            orchestrator.getRocketChatLogger().error('Error getting the categories from the Marketplace:', e);
                            return API.v1.internalError();
                        }

                        return API.v1.success(result);
                    }

                    if (
                        'buildExternalUrl' in this.queryParams &&
                        'appId' in this.queryParams &&
                        this.queryParams.buildExternalUrl &&
                        this.queryParams.appId
                    ) {
                        apiDeprecationLogger.endpoint(this.request.route, '7.0.0', this.response, 'Use /apps/buildExternalUrl to get the modal URLs.');
                        const workspaceId = settings.get('Cloud_Workspace_Id');

                        if (!this.queryParams.purchaseType || !purchaseTypes.has(this.queryParams.purchaseType)) {
                            return API.v1.failure({ error: 'Invalid purchase type' });
                        }

                        const token = await getWorkspaceAccessTokenWithScope('marketplace:purchase');
                        if (!token) {
                            return API.v1.failure({ error: 'Unauthorized' });
                        }

                        const subscribeRoute = this.queryParams.details === 'true' ? 'subscribe/details' : 'subscribe';

                        const seats = await Users.getActiveLocalUserCount();

                        return API.v1.success({
                            url: `${baseUrl}/apps/${this.queryParams.appId}/${
                                this.queryParams.purchaseType === 'buy' ? this.queryParams.purchaseType : subscribeRoute
                            }?workspaceId=${workspaceId}&token=${token.token}&seats=${seats}`,
                        });
                    }
                    apiDeprecationLogger.endpoint(this.request.route, '7.0.0', this.response, 'Use /apps/installed to get the installed apps list.');

                    const apps = manager.get().map(formatAppInstanceForRest);

                    return API.v1.success({ apps });
                },
                async post() {
                    let buff;
                    let marketplaceInfo;
                    let permissionsGranted;

                    if (this.bodyParams.url) {
                        try {
                            const response = await fetch(this.bodyParams.url);

                            if (response.status !== 200 || response.headers.get('content-type') !== 'application/zip') {
                                return API.v1.failure({
                                    error: 'Invalid url. It doesn\'t exist or is not "application/zip".',
                                });
                            }

                            buff = await response.buffer();
                        } catch (e: any) {
                            orchestrator.getRocketChatLogger().error('Error getting the app from url:', e.response.data);
                            return API.v1.internalError();
                        }

                        if (this.bodyParams.downloadOnly) {
                            apiDeprecationLogger.parameter(this.request.route, 'downloadOnly', '7.0.0', this.response);

                            return API.v1.success({ buff });
                        }
                    } else if ('appId' in this.bodyParams && this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) {
                        const baseUrl = orchestrator.getMarketplaceUrl();

                        const headers = getDefaultHeaders();
                        try {
                            const downloadToken = await getWorkspaceAccessToken(true, 'marketplace:download', false);
                            const marketplaceToken = await getWorkspaceAccessToken();

                            const [downloadResponse, marketplaceResponse] = await Promise.all([
                                fetch(`${baseUrl}/v2/apps/${this.bodyParams.appId}/download/${this.bodyParams.version}?token=${downloadToken}`, {
                                    headers,
                                }),
                                fetch(`${baseUrl}/v1/apps/${this.bodyParams.appId}?appVersion=${this.bodyParams.version}`, {
                                    headers: {
                                        Authorization: `Bearer ${marketplaceToken}`,
                                        ...headers,
                                    },
                                }),
                            ]);

                            if (downloadResponse.headers.get('content-type') !== 'application/zip') {
                                throw new Error('Invalid url. It doesn\'t exist or is not "application/zip".');
                            }

                            buff = Buffer.from(await downloadResponse.arrayBuffer());
                            marketplaceInfo = (await marketplaceResponse.json()) as any;
                            permissionsGranted = this.bodyParams.permissionsGranted;
                        } catch (err: any) {
                            return API.v1.failure(err.message);
                        }
                    } else {
                        const app = await getUploadFormData(
                            {
                                request: this.request,
                            },
                            { field: 'app', sizeLimit: settings.get('FileUpload_MaxFileSize') },
                        );

                        const { fields: formData } = app;

                        buff = app.fileBuffer;
                        permissionsGranted = (() => {
                            try {
                                const permissions = JSON.parse(formData?.permissions || '');
                                return permissions.length ? permissions : undefined;
                            } catch {
                                return undefined;
                            }
                        })();
                    }

                    if (!buff) {
                        return API.v1.failure({ error: 'Failed to get a file to install for the App. ' });
                    }

                    // Used mostly in Cloud hosting for security reasons
                    if (!marketplaceInfo && orchestrator.shouldDisablePrivateAppInstallation()) {
                        return API.v1.internalError('private_app_install_disabled');
                    }

                    const user = orchestrator
                        ?.getConverters()
                        ?.get('users')
                        ?.convertToApp(await Meteor.userAsync());

                    const aff = await manager.add(buff, { marketplaceInfo, permissionsGranted, enable: false, user });
                    const info: IAppInfo & { status?: AppStatus } = aff.getAppInfo();

                    if (aff.hasStorageError()) {
                        return API.v1.failure({ status: 'storage_error', messages: [aff.getStorageError()] });
                    }

                    if (aff.hasAppUserError()) {
                        return API.v1.failure({
                            status: 'app_user_error',
                            messages: [(aff.getAppUserError() as Record<string, any>).message],
                            payload: { username: (aff.getAppUserError() as Record<string, any>).username },
                        });
                    }

                    info.status = aff.getApp().getStatus();

                    void notifyAppInstall(orchestrator.getMarketplaceUrl() as string, 'install', info);

                    if (await canEnableApp(aff.getApp().getStorageItem())) {
                        const success = await manager.enable(info.id);
                        info.status = success ? AppStatus.AUTO_ENABLED : info.status;
                    }

                    void orchestrator.getNotifier().appAdded(info.id);

                    return API.v1.success({
                        app: info,
                        implemented: aff.getImplementedInferfaces(),
                        licenseValidation: aff.getLicenseValidationResult(),
                    });
                },
            },
        );

        this.api.addRoute(
            'buildExternalAppRequest',
            { authRequired: true },
            {
                async get() {
                    if (!this.queryParams.appId) {
                        return API.v1.failure({ error: 'Invalid request. Please ensure an appId is attached to the request.' });
                    }

                    const baseUrl = orchestrator.getMarketplaceUrl();
                    const workspaceId = settings.get<string>('Cloud_Workspace_Id');

                    const requester = {
                        id: this.user._id,
                        username: this.user.username,
                        name: this.user.name,
                        nickname: this.user.nickname,
                        emails: this.user?.emails?.map((e) => e.address),
                    };

                    let admins: {
                        id: string;
                        username?: string;
                        name?: string;
                        nickname?: string;
                    }[] = [];
                    try {
                        const adminsRaw = await Users.findUsersInRoles(['admin'], undefined, {
                            projection: {
                                username: 1,
                                name: 1,
                                nickname: 1,
                            },
                        }).toArray();

                        admins = adminsRaw.map((a) => {
                            return {
                                id: a._id,
                                username: a.username,
                                name: a.name,
                                nickname: a.nickname,
                            };
                        });
                    } catch (e) {
                        orchestrator.getRocketChatLogger().error('Error getting the admins to request an app be installed:', e);
                    }

                    const queryParams = new URLSearchParams();
                    queryParams.set('workspaceId', workspaceId);
                    queryParams.set('frameworkVersion', appsEngineVersionForMarketplace);
                    queryParams.set('requester', Buffer.from(JSON.stringify(requester)).toString('base64'));
                    queryParams.set('admins', Buffer.from(JSON.stringify(admins)).toString('base64'));

                    return API.v1.success({
                        url: `${baseUrl}/apps/${this.queryParams.appId}/requestAccess?${queryParams.toString()}`,
                    });
                },
            },
        );

        this.api.addRoute(
            'externalComponents',
            { authRequired: false },
            {
                get() {
                    const externalComponents = orchestrator.getProvidedComponents();

                    return API.v1.success({ externalComponents });
                },
            },
        );

        this.api.addRoute(
            'languages',
            { authRequired: false },
            {
                get() {
                    const apps = manager.get().map((prl) => ({
                        id: prl.getID(),
                        languages: prl.getStorageItem().languageContent,
                    }));

                    return API.v1.success({ apps });
                },
            },
        );

        this.api.addRoute(
            'externalComponentEvent',
            { authRequired: true },
            {
                post() {
                    if (
                        !this.bodyParams.externalComponent ||
                        !this.bodyParams.event ||
                        !['IPostExternalComponentOpened', 'IPostExternalComponentClosed'].includes(this.bodyParams.event)
                    ) {
                        return API.v1.failure({ error: 'Event and externalComponent must be provided.' });
                    }

                    try {
                        const { event, externalComponent } = this.bodyParams;
                        const result = (Apps?.getBridges()?.getListenerBridge() as Record<string, any>).externalComponentEvent(
                            event,
                            externalComponent,
                        );

                        return API.v1.success({ result });
                    } catch (e: any) {
                        orchestrator.getRocketChatLogger().error(`Error triggering external components' events ${e.response.data}`);
                        return API.v1.internalError();
                    }
                },
            },
        );

        this.api.addRoute(
            'bundles/:id/apps',
            { authRequired: true, permissionsRequired: ['manage-apps'] },
            {
                async get() {
                    const baseUrl = orchestrator.getMarketplaceUrl();

                    const headers: Record<string, any> = {};
                    const token = await getWorkspaceAccessToken();
                    if (token) {
                        headers.Authorization = `Bearer ${token}`;
                    }

                    let result;
                    try {
                        const request = await fetch(`${baseUrl}/v1/bundles/${this.urlParams.id}/apps`, { headers });
                        if (request.status !== 200) {
                            orchestrator.getRocketChatLogger().error("Error getting the Bundle's Apps from the Marketplace:", await request.json());
                            return API.v1.failure();
                        }
                        result = await request.json();
                    } catch (e: any) {
                        orchestrator.getRocketChatLogger().error("Error getting the Bundle's Apps from the Marketplace:", e.response.data);
                        return API.v1.internalError();
                    }

                    return API.v1.success({ apps: result });
                },
            },
        );

        this.api.addRoute(
            'featured-apps',
            { authRequired: true },
            {
                async get() {
                    const baseUrl = orchestrator.getMarketplaceUrl();

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

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

                    return API.v1.success(result);
                },
            },
        );

        this.api.addRoute(
            ':id',
            { authRequired: true, permissionsRequired: ['manage-apps'] },
            {
                async get() {
                    if (this.queryParams.marketplace && this.queryParams.version) {
                        const baseUrl = orchestrator.getMarketplaceUrl();

                        const headers: Record<string, any> = {}; // DO NOT ATTACH THE FRAMEWORK/ENGINE VERSION HERE.
                        const token = await getWorkspaceAccessToken();
                        if (token) {
                            headers.Authorization = `Bearer ${token}`;
                        }

                        let result: any;
                        try {
                            const request = await fetch(`${baseUrl}/v1/apps/${this.urlParams.id}?appVersion=${this.queryParams.version}`, { headers });
                            if (request.status !== 200) {
                                orchestrator.getRocketChatLogger().error('Error getting the App information from the Marketplace:', await request.json());
                                return API.v1.failure();
                            }
                            result = await request.json();
                        } catch (e) {
                            return handleError('Unable to access Marketplace. Does the server has access to the internet?', e);
                        }

                        return API.v1.success({ app: result[0] });
                    }

                    if (this.queryParams.marketplace && this.queryParams.update && this.queryParams.appVersion) {
                        const baseUrl = orchestrator.getMarketplaceUrl();

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

                        let result;
                        try {
                            const request = await fetch(`${baseUrl}/v1/apps/${this.urlParams.id}/latest?appVersion=${this.queryParams.appVersion}`, {
                                headers,
                            });
                            if (request.status !== 200) {
                                orchestrator.getRocketChatLogger().error('Error getting the App update info from the Marketplace:', await request.json());
                                return API.v1.failure();
                            }
                            result = await request.json();
                        } catch (e) {
                            return handleError('Unable to access Marketplace. Does the server has access to the internet?', e);
                        }

                        return API.v1.success({ app: result });
                    }
                    const app = manager.getOneById(this.urlParams.id);
                    if (!app) {
                        return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`);
                    }

                    return API.v1.success({
                        app: formatAppInstanceForRest(app),
                    });
                },
                async post() {
                    let buff;
                    let permissionsGranted;
                    let isPrivateAppUpload = false;

                    if (this.bodyParams.url) {
                        const response = await fetch(this.bodyParams.url);

                        if (response.status !== 200 || response.headers.get('content-type') !== 'application/zip') {
                            return API.v1.failure({
                                error: 'Invalid url. It doesn\'t exist or is not "application/zip".',
                            });
                        }

                        buff = Buffer.from(await response.arrayBuffer());
                    } else if (this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) {
                        const baseUrl = orchestrator.getMarketplaceUrl();

                        const headers = getDefaultHeaders();
                        const token = await getWorkspaceAccessToken(true, 'marketplace:download', false);

                        try {
                            const response = await fetch(
                                `${baseUrl}/v2/apps/${this.bodyParams.appId}/download/${this.bodyParams.version}?token=${token}`,
                                {
                                    headers,
                                },
                            );

                            if (response.status !== 200) {
                                orchestrator.getRocketChatLogger().error('Error getting the App from the Marketplace:', await response.text());
                                return API.v1.failure();
                            }

                            if (response.headers.get('content-type') !== 'application/zip') {
                                return API.v1.failure({
                                    error: 'Invalid url. It doesn\'t exist or is not "application/zip".',
                                });
                            }

                            buff = Buffer.from(await response.arrayBuffer());
                        } catch (e: any) {
                            orchestrator.getRocketChatLogger().error('Error getting the App from the Marketplace:', e.response.data);
                            return API.v1.internalError();
                        }
                    } else {
                        isPrivateAppUpload = true;

                        const app = await getUploadFormData(
                            {
                                request: this.request,
                            },
                            { field: 'app', sizeLimit: settings.get('FileUpload_MaxFileSize') },
                        );

                        const { fields: formData } = app;

                        buff = app.fileBuffer;
                        permissionsGranted = (() => {
                            try {
                                const permissions = JSON.parse(formData?.permissions || '');
                                return permissions.length ? permissions : undefined;
                            } catch {
                                return undefined;
                            }
                        })();
                    }

                    if (!buff) {
                        return API.v1.failure({ error: 'Failed to get a file to install for the App. ' });
                    }

                    if (isPrivateAppUpload && orchestrator.shouldDisablePrivateAppInstallation()) {
                        return API.v1.internalError('private_app_install_disabled');
                    }

                    const aff = await manager.update(buff, permissionsGranted);
                    const info: IAppInfo & { status?: AppStatus } = aff.getAppInfo();

                    if (aff.hasStorageError()) {
                        return API.v1.failure({ status: 'storage_error', messages: [aff.getStorageError()] });
                    }

                    if (aff.hasAppUserError()) {
                        return API.v1.failure({
                            status: 'app_user_error',
                            messages: [(aff.getAppUserError() as Record<string, any>).message],
                            payload: { username: (aff.getAppUserError() as Record<string, any>).username },
                        });
                    }

                    info.status = aff.getApp().getStatus();

                    void notifyAppInstall(orchestrator.getMarketplaceUrl() as string, 'update', info);

                    void orchestrator.getNotifier().appUpdated(info.id);

                    return API.v1.success({
                        app: info,
                        implemented: aff.getImplementedInferfaces(),
                        licenseValidation: aff.getLicenseValidationResult(),
                    });
                },
                async delete() {
                    const prl = manager.getOneById(this.urlParams.id);

                    if (!prl) {
                        return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`);
                    }

                    const user = orchestrator
                        ?.getConverters()
                        ?.get('users')
                        .convertToApp(await Meteor.userAsync());

                    await manager.remove(prl.getID(), { user });

                    const info: IAppInfo & { status?: AppStatus } = prl.getInfo();
                    info.status = prl.getStatus();

                    void notifyAppInstall(orchestrator.getMarketplaceUrl() as string, 'uninstall', info);

                    return API.v1.success({ app: info });
                },
            },
        );

        this.api.addRoute(
            ':id/versions',
            { authRequired: true },
            {
                async get() {
                    const baseUrl = orchestrator.getMarketplaceUrl();

                    const headers: Record<string, any> = {}; // DO NOT ATTACH THE FRAMEWORK/ENGINE VERSION HERE.
                    const token = await getWorkspaceAccessToken();
                    if (token) {
                        headers.Authorization = `Bearer ${token}`;
                    }

                    let result;
                    let statusCode;
                    try {
                        const request = await fetch(`${baseUrl}/v1/apps/${this.urlParams.id}`, { headers });
                        statusCode = request.status;
                        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);
                    }

                    if (!result || statusCode !== 200) {
                        orchestrator.getRocketChatLogger().error('Error getting the App versions from the Marketplace:', result);
                        return API.v1.failure();
                    }

                    return API.v1.success({ apps: result });
                },
            },
        );

        this.api.addRoute(
            'notify-admins',
            { authRequired: true },
            {
                async post() {
                    const { appId, appName, appVersion, message } = this.bodyParams;
                    const workspaceUrl = settings.get<string>('Site_Url');

                    const regex = new RegExp('\\/$', 'gm');
                    const safeWorkspaceUrl = workspaceUrl.replace(regex, '');
                    const learnMore = `${safeWorkspaceUrl}/marketplace/explore/info/${appId}/${appVersion}/requests`;

                    try {
                        const msgs: (params: { adminUser: IUser }) => Promise<Partial<IMessage>> = async ({ adminUser }) => {
                            return {
                                msg: i18n.t('App_Request_Admin_Message', {
                                    admin_name: adminUser.name || '',
                                    app_name: appName || '',
                                    user_name: `@${this.user.username}`,
                                    message: message || '',
                                    learn_more: learnMore,
                                    interpolation: { escapeValue: false },
                                }),
                            };
                        };

                        await sendMessagesToAdmins({ msgs });

                        return API.v1.success();
                    } catch (e) {
                        orchestrator.getRocketChatLogger().error('Error when notifying admins that an user requested an app:', e);
                        return API.v1.failure();
                    }
                },
            },
        );

        this.api.addRoute(
            ':id/sync',
            { authRequired: true, permissionsRequired: ['manage-apps'] },
            {
                async post() {
                    const baseUrl = orchestrator.getMarketplaceUrl();

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

                    const workspaceIdSetting = await Settings.findOneById('Cloud_Workspace_Id');
                    if (!workspaceIdSetting) {
                        return API.v1.failure('No workspace id found');
                    }

                    let result;
                    let statusCode;
                    try {
                        const request = await fetch(`${baseUrl}/v1/workspaces/${workspaceIdSetting.value}/apps/${this.urlParams.id}`, { headers });
                        statusCode = request.status;
                        result = await request.json();

                        if (!request.ok) {
                            throw new Error(result.error);
                        }
                    } catch (e: any) {
                        orchestrator.getRocketChatLogger().error('Error syncing the App from the Marketplace:', e);
                        return API.v1.internalError();
                    }

                    if (statusCode !== 200) {
                        orchestrator.getRocketChatLogger().error('Error syncing the App from the Marketplace:', result);
                        return API.v1.failure();
                    }

                    await Apps.updateAppsMarketplaceInfo([result]);

                    return API.v1.success({ app: result });
                },
            },
        );

        this.api.addRoute(
            ':id/icon',
            { authRequired: false },
            {
                get() {
                    const prl = manager.getOneById(this.urlParams.id);
                    if (!prl) {
                        return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`);
                    }

                    const info = prl.getInfo();
                    if (!info?.iconFileContent) {
                        return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`);
                    }

                    const imageData = info.iconFileContent.split(';base64,');

                    const buf = Buffer.from(imageData[1], 'base64');

                    return {
                        statusCode: 200,
                        headers: {
                            'Content-Length': buf.length,
                            'Content-Type': imageData[0].replace('data:', ''),
                        },
                        body: buf,
                    };
                },
            },
        );

        this.api.addRoute(
            ':id/screenshots',
            { authRequired: false },
            {
                async get() {
                    const baseUrl = orchestrator.getMarketplaceUrl();
                    const appId = this.urlParams.id;
                    const headers = getDefaultHeaders();

                    try {
                        const request = await fetch(`${baseUrl}/v1/apps/${appId}/screenshots`, { headers });
                        const data = await request.json();

                        return API.v1.success({
                            screenshots: data,
                        });
                    } catch (e: any) {
                        orchestrator.getRocketChatLogger().error('Error getting the screenshots from the Marketplace:', e.message);
                        return API.v1.failure(e.message);
                    }
                },
            },
        );

        this.api.addRoute(
            ':id/languages',
            { authRequired: false },
            {
                get() {
                    const prl = manager.getOneById(this.urlParams.id);

                    if (prl) {
                        const languages = prl.getStorageItem().languageContent || {};

                        return API.v1.success({ languages });
                    }
                    return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`);
                },
            },
        );

        this.api.addRoute(
            ':id/logs',
            { authRequired: true, permissionsRequired: ['manage-apps'] },
            {
                async get() {
                    const prl = manager.getOneById(this.urlParams.id);

                    if (prl) {
                        const { offset, count } = await getPaginationItems(this.queryParams);
                        const { sort, fields, query } = await this.parseJsonQuery();

                        const ourQuery = Object.assign({}, query, { appId: prl.getID() });
                        const options = {
                            sort: sort || { _updatedAt: -1 },
                            skip: offset,
                            limit: count,
                            fields,
                        };

                        const logs = await orchestrator?.getLogStorage()?.find(ourQuery, options);

                        return API.v1.success({ logs });
                    }
                    return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`);
                },
            },
        );

        this.api.addRoute(
            ':id/settings',
            { authRequired: true, permissionsRequired: ['manage-apps'] },
            {
                get() {
                    const prl = manager.getOneById(this.urlParams.id);

                    if (prl) {
                        const settings = Object.assign({}, prl.getStorageItem().settings);

                        Object.keys(settings).forEach((k) => {
                            if (settings[k].hidden) {
                                delete settings[k];
                            }
                        });

                        return API.v1.success({ settings });
                    }
                    return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`);
                },
                async post() {
                    if (!this.bodyParams?.settings) {
                        return API.v1.failure('The settings to update must be present.');
                    }

                    const prl = manager.getOneById(this.urlParams.id);

                    if (!prl) {
                        return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`);
                    }

                    const { settings } = prl.getStorageItem();

                    const updated = [];

                    for await (const s of this.bodyParams.settings) {
                        if (settings[s.id] && settings[s.id].value !== s.value) {
                            await manager.getSettingsManager().updateAppSetting(this.urlParams.id, s);
                            // Updating?
                            updated.push(s);
                        }
                    }

                    return API.v1.success({ updated });
                },
            },
        );

        this.api.addRoute(
            ':id/settings/:settingId',
            { authRequired: true, permissionsRequired: ['manage-apps'] },
            {
                get() {
                    try {
                        const setting = manager.getSettingsManager().getAppSetting(this.urlParams.id, this.urlParams.settingId);

                        return API.v1.success({ setting });
                    } catch (e: any) {
                        if (e.message.includes('No setting found')) {
                            return API.v1.notFound(`No Setting found on the App by the id of: "${this.urlParams.settingId}"`);
                        }
                        if (e.message.includes('No App found')) {
                            return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`);
                        }
                        return API.v1.failure(e.message);
                    }
                },
                async post() {
                    if (!this.bodyParams.setting) {
                        return API.v1.failure('Setting to update to must be present on the posted body.');
                    }

                    try {
                        await manager.getSettingsManager().updateAppSetting(this.urlParams.id, this.bodyParams.setting);

                        return API.v1.success();
                    } catch (e: any) {
                        if (e.message.includes('No setting found')) {
                            return API.v1.notFound(`No Setting found on the App by the id of: "${this.urlParams.settingId}"`);
                        }
                        if (e.message.includes('No App found')) {
                            return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`);
                        }
                        return API.v1.failure(e.message);
                    }
                },
            },
        );

        this.api.addRoute(
            ':id/apis',
            { authRequired: true, permissionsRequired: ['manage-apps'] },
            {
                get() {
                    const prl = manager.getOneById(this.urlParams.id);

                    if (prl) {
                        return API.v1.success({
                            apis: (manager as Record<string, any>).apiManager.listApis(this.urlParams.id), // TODO: this is accessing a private property from the manager, we should expose a method to get the list of APIs
                        });
                    }
                    return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`);
                },
            },
        );

        this.api.addRoute(
            ':id/status',
            { authRequired: true, permissionsRequired: ['manage-apps'] },
            {
                get() {
                    const prl = manager.getOneById(this.urlParams.id);

                    if (prl) {
                        return API.v1.success({ status: prl.getStatus() });
                    }
                    return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`);
                },
                async post() {
                    const { id: appId } = this.urlParams;
                    const { status } = this.bodyParams;

                    if (!status || typeof status !== 'string') {
                        return API.v1.failure('Invalid status provided, it must be "status" field and a string.');
                    }

                    const prl = manager.getOneById(appId);
                    if (!prl) {
                        return API.v1.notFound(`No App found by the id of: ${appId}`);
                    }

                    const storedApp = prl.getStorageItem();
                    const { installationSource, marketplaceInfo } = storedApp;

                    if (!License.hasValidLicense() && installationSource === AppInstallationSource.MARKETPLACE) {
                        try {
                            const baseUrl = orchestrator.getMarketplaceUrl() as string;
                            const headers = getDefaultHeaders();
                            const { version } = prl.getInfo();

                            await appEnableCheck({
                                baseUrl,
                                headers,
                                appId,
                                version,
                                marketplaceInfo,
                                status,
                                logger: orchestrator.getRocketChatLogger(),
                            });
                        } catch (error: any) {
                            return API.v1.failure(error.message);
                        }
                    }

                    if (AppStatusUtils.isEnabled(status) && !(await canEnableApp(storedApp))) {
                        return API.v1.failure('Enabled apps have been maxed out');
                    }

                    const result = await manager.changeStatus(prl.getID(), status);
                    return API.v1.success({ status: result.getStatus() });
                },
            },
        );

        this.api.addRoute(
            'app-request',
            { authRequired: true },
            {
                async get() {
                    const baseUrl = orchestrator.getMarketplaceUrl();
                    const { appId, q = '', sort = '', limit = 25, offset = 0 } = this.queryParams;
                    const headers = getDefaultHeaders();

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

                    try {
                        const request = await fetch(`${baseUrl}/v1/app-request?appId=${appId}&q=${q}&sort=${sort}&limit=${limit}&offset=${offset}`, {
                            headers,
                        });
                        const result = await request.json();

                        if (!request.ok) {
                            throw new Error(result.error);
                        }
                        return API.v1.success(result);
                    } catch (e: any) {
                        orchestrator.getRocketChatLogger().error('Error getting all non sent app requests from the Marketplace:', e.message);

                        return API.v1.failure(e.message);
                    }
                },
            },
        );

        this.api.addRoute(
            'app-request/stats',
            { authRequired: true },
            {
                async get() {
                    const baseUrl = orchestrator.getMarketplaceUrl();
                    const headers = getDefaultHeaders();

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

                    try {
                        const request = await fetch(`${baseUrl}/v1/app-request/stats`, { headers });
                        const result = await request.json();
                        if (!request.ok) {
                            throw new Error(result.error);
                        }
                        return API.v1.success(result);
                    } catch (e: any) {
                        orchestrator.getRocketChatLogger().error('Error getting the app requests stats from marketplace', e.message);

                        return API.v1.failure(e.message);
                    }
                },
            },
        );

        this.api.addRoute(
            'app-request/markAsSeen',
            { authRequired: true },
            {
                async post() {
                    const baseUrl = orchestrator.getMarketplaceUrl();
                    const headers = getDefaultHeaders();

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

                    const { unseenRequests } = this.bodyParams;

                    try {
                        const request = await fetch(`${baseUrl}/v1/app-request/markAsSeen`, {
                            method: 'POST',
                            headers,
                            body: { ids: unseenRequests },
                        });
                        const result = await request.json();

                        if (!request.ok) {
                            throw new Error(result.error);
                        }

                        return API.v1.success(result);
                    } catch (e: any) {
                        orchestrator.getRocketChatLogger().error('Error marking app requests as seen', e.message);

                        return API.v1.failure(e.message);
                    }
                },
            },
        );
    }
}