Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] Notify admins via rocket.cat when a user requests to use an app #27858

Merged
merged 8 commits into from
Feb 7, 2023
38 changes: 38 additions & 0 deletions apps/meteor/app/apps/server/communication/rest.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { HTTP } from 'meteor/http';
import { Settings, Users as UsersRaw } from '@rocket.chat/models';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';

import { API } from '../../../api/server';
import { getUploadFormData } from '../../../api/server/lib/getUploadFormData';
Expand All @@ -14,6 +15,7 @@ import { actionButtonsHandler } from './endpoints/actionButtonsHandler';
import { fetch } from '../../../../server/lib/http/fetch';
import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';
import { notifyAppInstall } from '../marketplace/appInstall';
import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmins';

const rocketChatVersion = Info.version;
const appsEngineVersionForMarketplace = Info.marketplaceApiVersion.replace(/-.*/g, '');
Expand Down Expand Up @@ -807,6 +809,42 @@ export class AppsRestApi {
},
);

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

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

try {
const msgs = ({ adminUser }) => {
return {
msg: TAPi18n.__('App_Request_Admin_Message', {
admin_name: adminUser.name,
app_name: appName,
user_name: this.user.name || this.user.username,
message,
learn_more: learnMore,
}),
};
};

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'] },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const appRequestNotififyForUsers = async (
// Calculate the number of loops - 1 because the first request was already made
const loops = Math.ceil(total / pagination.limit) - 1;
const requestsCollection = [];
const learnMore = `${workspaceUrl}admin/marketplace/all/info/${appId}`;
const learnMore = `${workspaceUrl}marketplace/explore/info/${appId}`;

// Notify first batch
requestsCollection.push(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import type { App } from '@rocket.chat/core-typings';
import { Box, Button, Icon, Throbber, Tag, Margins } from '@rocket.chat/fuselage';
import { useSafely } from '@rocket.chat/fuselage-hooks';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useRouteParameter, usePermission, useSetModal, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
import {
useRouteParameter,
usePermission,
useSetModal,
useMethod,
useTranslation,
useToastMessageDispatch,
useEndpoint,
} from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { useCallback, useState, memo } from 'react';

Expand All @@ -15,6 +23,11 @@ import { appButtonProps, appMultiStatusProps, handleAPIError, handleInstallError
import { marketplaceActions } from '../../../helpers/marketplaceActions';
import AppStatusPriceDisplay from './AppStatusPriceDisplay';

type AppRequestPostMessage = {
message: string;
status: string;
};

type AppStatusProps = {
app: App;
showStatus?: boolean;
Expand All @@ -24,6 +37,7 @@ type AppStatusProps = {

const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...props }: AppStatusProps): ReactElement => {
const t = useTranslation();
const [endUserRequested, setEndUserRequested] = useState(false);
const [loading, setLoading] = useSafely(useState(false));
const [isAppPurchased, setPurchased] = useSafely(useState(app?.isPurchased));
const setModal = useSetModal();
Expand All @@ -39,6 +53,22 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro

const totalSeenRequests = app?.appRequestStats?.totalSeen;
const totalUnseenRequests = app?.appRequestStats?.totalUnseen;
const dispatchToastMessage = useToastMessageDispatch();

const notifyAdmins = useEndpoint('POST', '/apps/notify-admins');
const requestConfirmAction = (postMessage: AppRequestPostMessage) => {
setModal(null);
setLoading(false);
dispatchToastMessage({ type: 'success', message: 'App request submitted' });

setEndUserRequested(true);

notifyAdmins({
appId: app.id,
appName: app.name,
message: postMessage.message,
});
};

if (button?.action === undefined && button?.action) {
throw new Error('action must not be null');
Expand Down Expand Up @@ -123,7 +153,7 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro
if (action === 'request') {
try {
const data = await Apps.buildExternalAppRequest(app.id);
setModal(<IframeModal url={data?.url} cancel={cancelAction} confirm={undefined} />);
setModal(<IframeModal url={data?.url} wrapperHeight={'x380'} cancel={cancelAction} confirm={requestConfirmAction} />);
} catch (error) {
handleAPIError(error);
}
Expand Down Expand Up @@ -180,7 +210,13 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro
borderRadius='x4'
invisible={!showStatus && !loading}
>
<Button primary small disabled={loading || (action === 'request' && app.requestedEndUser)} onClick={handleClick} mie='x8'>
<Button
primary
small
disabled={loading || (action === 'request' && (app?.requestedEndUser || endUserRequested))}
onClick={handleClick}
mie='x8'
>
{loading ? (
<Throbber inheritColor />
) : (
Expand Down
22 changes: 19 additions & 3 deletions apps/meteor/client/views/marketplace/AppMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ function AppMenu({ app, isAppDetailsPage, ...props }) {
const buildExternalUrl = useEndpoint('GET', '/apps');
const syncApp = useEndpoint('POST', `/apps/${app.id}/sync`);
const uninstallApp = useEndpoint('DELETE', `/apps/${app.id}`);
const notifyAdmins = useEndpoint('POST', `/apps/notify-admins`);

const [loading, setLoading] = useState(false);
const [requestedEndUser, setRequestedEndUser] = useState(app.requestedEndUser);

const canAppBeSubscribed = app.purchaseType === 'subscription';
const isSubscribed = app.subscriptionInfo && ['active', 'trialing'].includes(app.subscriptionInfo.status);
Expand All @@ -77,6 +79,19 @@ function AppMenu({ app, isAppDetailsPage, ...props }) {
[setModal, action, app, setLoading],
);

const requestConfirmAction = (postMessage) => {
setModal(null);
setLoading(false);
setRequestedEndUser(true);
dispatchToastMessage({ type: 'success', message: 'App request submitted' });

notifyAdmins({
appId: app.id,
appName: app.name,
message: postMessage.message,
});
};

const showAppPermissionsReviewModal = useCallback(() => {
if (!isAppPurchased) {
setPurchased(true);
Expand Down Expand Up @@ -142,7 +157,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) {
if (action === 'request') {
try {
const data = await Apps.buildExternalAppRequest(app.id);
setModal(<IframeModal url={data.url} cancel={cancelAction} confirm={undefined} />);
setModal(<IframeModal url={data.url} wrapperHeight={'x380'} cancel={cancelAction} confirm={requestConfirmAction} />);
} catch (error) {
handleAPIError(error);
}
Expand Down Expand Up @@ -311,12 +326,12 @@ function AppMenu({ app, isAppDetailsPage, ...props }) {
...(!app.installed && {
acquire: {
label: (
<Option disabled={app.requestedEndUser}>
<Option disabled={requestedEndUser}>
{isAdminUser && <Icon name={incompatibleIconName(app, 'install')} size='x16' marginInlineEnd='x4' />}
{t(button.label.replace(' ', '_'))}
</Option>
),
action: app.requestedEndUser ? () => {} : handleAcquireApp,
action: requestedEndUser ? () => {} : handleAcquireApp,
},
}),
};
Expand Down Expand Up @@ -401,6 +416,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) {
};
}, [
canAppBeSubscribed,
requestedEndUser,
isSubscribed,
incompatibleIconName,
app,
Expand Down
6 changes: 3 additions & 3 deletions apps/meteor/client/views/marketplace/IframeModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const iframeMsgListener = (confirm, cancel) => (e) => {
data.result ? confirm(data) : cancel();
};

const IframeModal = ({ url, confirm, cancel, ...props }) => {
const IframeModal = ({ url, confirm, cancel, wrapperHeight = 'x360', ...props }) => {
useEffect(() => {
const listener = iframeMsgListener(confirm, cancel);

Expand All @@ -24,8 +24,8 @@ const IframeModal = ({ url, confirm, cancel, ...props }) => {
}, [confirm, cancel]);

return (
<Modal height='x360' {...props}>
<Box padding='x12' w='full' h='full' flexGrow={1}>
<Modal height={wrapperHeight} {...props}>
<Box padding='x12' w='full' h='full' flexGrow={1} bg='white' borderRadius='x8'>
<iframe style={{ border: 'none', height: '100%', width: '100%' }} src={url} />
</Box>
</Modal>
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -5128,6 +5128,7 @@
"Verify_your_email_for_the_code_we_sent": "Verify your email for the code we sent",
"Version": "Version",
"Version_version": "Version __version__",
"App_Request_Admin_Message": "Hi __admin_name__, __user_name__ submitted a request to install __app_name__ app on this workspace. \n \n This is the message they included: \n>__message__ \n \n To learn more and install the __app_name__ app, [click here](__learn_more__).",
"App_version_incompatible_tooltip": "App incompatible with Rocket.Chat version",
"App_request_enduser_message": "The app you requested, __appname__ has just been installed on this workspace. [Click here to learn more](__learnmore__).",
"App_requests_by_workspace": "App requests by workspace members appear here",
Expand Down
4 changes: 4 additions & 0 deletions packages/rest-typings/src/apps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ export type AppsEndpoints = {
POST: (params: { unseenRequests: Array<string> }) => { succes: boolean };
};

'/apps/notify-admins': {
POST: (params: { appId: string; appName: string; message: string }) => void;
};

'/apps': {
GET:
| ((params: { buildExternalUrl: 'true'; purchaseType?: 'buy' | 'subscription'; appId?: string; details?: 'true' | 'false' }) => {
Expand Down