Преглед изворни кода

Merge pull request #4281 from weseek/feat/setting-permission-in-slack-integration

Feat/setting permission in slack integration
Sizma yosimaz пре 4 година
родитељ
комит
a49a97e48a

+ 2 - 0
packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx

@@ -50,6 +50,7 @@ const CustomBotWithoutProxySettings = (props) => {
           slackSigningSecret={props.slackSigningSecret}
           onTestConnectionInvoked={props.onTestConnectionInvoked}
           onUpdatedSecretToken={props.onUpdatedSecretToken}
+          commandPermission={props.commandPermission}
         />
       </div>
     </>
@@ -69,6 +70,7 @@ CustomBotWithoutProxySettings.propTypes = {
   onUpdatedSecretToken: PropTypes.func.isRequired,
   onTestConnectionInvoked: PropTypes.func.isRequired,
   connectionStatuses: PropTypes.object.isRequired,
+  commandPermission: PropTypes.object,
 };
 
 export default CustomBotWithoutProxySettingsWrapper;

+ 14 - 2
packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -7,6 +7,7 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 import MessageBasedOnConnection from './MessageBasedOnConnection';
 import CustomBotWithoutProxySecretTokenSection from './CustomBotWithoutProxySecretTokenSection';
 import { addLogs } from './slak-integration-util';
+import ManageCommandsProcessWithoutProxy from './ManageCommandsProcessWithoutProxy';
 
 
 export const botInstallationStep = {
@@ -20,7 +21,7 @@ export const botInstallationStep = {
 const CustomBotWithoutProxySettingsAccordion = (props) => {
   const {
     appContainer, activeStep, onTestConnectionInvoked,
-    slackSigningSecret, slackBotToken, slackSigningSecretEnv, slackBotTokenEnv,
+    slackSigningSecret, slackBotToken, slackSigningSecretEnv, slackBotTokenEnv, commandPermission,
   } = props;
   const successMessage = 'Successfully sent to Slack workspace.';
 
@@ -124,7 +125,17 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CONNECTION_TEST)}
         // eslint-disable-next-line max-len
-        title={<><span className="mr-2">④</span>{t('admin:slack_integration.accordion.test_connection')}{isLatestConnectionSuccess && <i className="ml-3 text-success fa fa-check"></i>}</>}
+        title={<><span className="mr-2">④</span>{t('admin:slack_integration.accordion.manage_commands')}{isLatestConnectionSuccess && <i className="ml-3 text-success fa fa-check"></i>}</>}
+      >
+        <ManageCommandsProcessWithoutProxy
+          commandPermission={props.commandPermission}
+          apiv3Put={props.appContainer.apiv3.put}
+        />
+      </Accordion>
+      <Accordion
+        defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CONNECTION_TEST)}
+        // eslint-disable-next-line max-len
+        title={<><span className="mr-2">⑤</span>{t('admin:slack_integration.accordion.test_connection')}{isLatestConnectionSuccess && <i className="ml-3 text-success fa fa-check"></i>}</>}
       >
         <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
         <div className="d-flex justify-content-center">
@@ -185,6 +196,7 @@ CustomBotWithoutProxySettingsAccordion.propTypes = {
   slackSigningSecretEnv: PropTypes.string,
   slackBotToken: PropTypes.string,
   slackBotTokenEnv: PropTypes.string,
+  commandPermission: PropTypes.object,
 
 };
 

+ 242 - 0
packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcessWithoutProxy.jsx

@@ -0,0 +1,242 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse } from '@growi/slack';
+import loggerFactory from '~/utils/logger';
+
+import { toastSuccess, toastError } from '../../../client/util/apiNotification';
+
+const logger = loggerFactory('growi:SlackIntegration:ManageCommandsProcess');
+
+const PermissionTypes = {
+  ALLOW_ALL: 'allowAll',
+  DENY_ALL: 'denyAll',
+  ALLOW_SPECIFIED: 'allowSpecified',
+};
+
+const defaultCommandsName = [...defaultSupportedCommandsNameForBroadcastUse, ...defaultSupportedCommandsNameForSingleUse];
+
+
+// A utility function that returns the new state but identical to the previous state
+const getUpdatedChannelsList = (commandPermissionObj, commandName, value) => {
+  // string to array
+  const allowedChannelsArray = value.split(',');
+  // trim whitespace from all elements
+  const trimedAllowedChannelsArray = allowedChannelsArray.map(channelName => channelName.trim());
+
+  commandPermissionObj[commandName] = trimedAllowedChannelsArray;
+  return commandPermissionObj;
+};
+
+// A utility function that returns the new state
+const getUpdatedPermissionSettings = (commandPermissionObj, commandName, value) => {
+  const editedCommandPermissionObj = { ...commandPermissionObj };
+  switch (value) {
+    case PermissionTypes.ALLOW_ALL:
+      editedCommandPermissionObj[commandName] = true;
+      break;
+    case PermissionTypes.DENY_ALL:
+      editedCommandPermissionObj[commandName] = false;
+      break;
+    case PermissionTypes.ALLOW_SPECIFIED:
+      editedCommandPermissionObj[commandName] = [];
+      break;
+    default:
+      logger.error('Not implemented');
+      break;
+  }
+  return editedCommandPermissionObj;
+};
+
+
+const PermissionSettingForEachCommandComponent = ({
+  commandName, editingCommandPermission, onPermissionTypeClicked, onPermissionListChanged,
+}) => {
+  const { t } = useTranslation();
+
+  if (editingCommandPermission == null) {
+    return null;
+  }
+
+  function permissionTypeClickHandler(e) {
+    if (onPermissionTypeClicked == null) {
+      return;
+    }
+    onPermissionTypeClicked(e);
+  }
+
+  function onPermissionListChangeHandler(e) {
+    if (onPermissionListChanged == null) {
+      return;
+    }
+    onPermissionListChanged(e);
+  }
+
+  const permission = editingCommandPermission[commandName];
+  const hiddenClass = Array.isArray(permission) ? '' : 'd-none';
+  const textareaDefaultValue = Array.isArray(permission) ? permission.join(',') : '';
+
+
+  return (
+    <div className="my-1 mb-2">
+      <div className="row align-items-center mb-3">
+        <p className="col my-auto text-capitalize align-middle">{commandName}</p>
+        <div className="col dropdown">
+          <button
+            className="btn btn-outline-secondary dropdown-toggle text-right col-12 col-md-auto"
+            type="button"
+            id="dropdownMenuButton"
+            data-toggle="dropdown"
+            aria-haspopup="true"
+            aria-expanded="true"
+          >
+            <span className="float-left">
+              {permission === true && t('admin:slack_integration.accordion.allow_all')}
+              {permission === false && t('admin:slack_integration.accordion.deny_all')}
+              {Array.isArray(permission) && t('admin:slack_integration.accordion.allow_specified')}
+            </span>
+          </button>
+          <div className="dropdown-menu">
+            <button
+              className="dropdown-item"
+              type="button"
+              name={commandName}
+              value={PermissionTypes.ALLOW_ALL}
+              onClick={e => permissionTypeClickHandler(e)}
+            >
+              {t('admin:slack_integration.accordion.allow_all_long')}
+            </button>
+            <button
+              className="dropdown-item"
+              type="button"
+              name={commandName}
+              value={PermissionTypes.DENY_ALL}
+              onClick={e => permissionTypeClickHandler(e)}
+            >
+              {t('admin:slack_integration.accordion.deny_all_long')}
+            </button>
+            <button
+              className="dropdown-item"
+              type="button"
+              name={commandName}
+              value={PermissionTypes.ALLOW_SPECIFIED}
+              onClick={e => permissionTypeClickHandler(e)}
+            >
+              {t('admin:slack_integration.accordion.allow_specified_long')}
+            </button>
+          </div>
+        </div>
+      </div>
+      <div className={`row-12 row-md-6 ${hiddenClass}`}>
+        <textarea
+          className="form-control"
+          type="textarea"
+          name={commandName}
+          value={textareaDefaultValue}
+          onChange={e => onPermissionListChangeHandler(e)}
+        />
+        <p className="form-text text-muted small">
+          {t('admin:slack_integration.accordion.allowed_channels_description', { commandName })}
+          <br />
+        </p>
+      </div>
+    </div>
+  );
+};
+
+PermissionSettingForEachCommandComponent.propTypes = {
+  commandName: PropTypes.string,
+  editingCommandPermission: PropTypes.object,
+  onPermissionTypeClicked: PropTypes.func,
+  onPermissionListChanged: PropTypes.func,
+};
+
+
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+const ManageCommandsProcessWithoutProxy = ({ apiv3Put, commandPermission }) => {
+  const { t } = useTranslation();
+  const [editingCommandPermission, setEditingCommandPermission] = useState({});
+
+  const updatePermissionsCommandsState = useCallback((e) => {
+    const { target } = e;
+    const { name: commandName, value } = target;
+
+    // update state
+    setEditingCommandPermission(commandPermissionObj => getUpdatedPermissionSettings(commandPermissionObj, commandName, value));
+  }, []);
+
+
+  useEffect(() => {
+    if (commandPermission == null) {
+      return;
+    }
+    const updatedState = { ...commandPermission };
+    setEditingCommandPermission(updatedState);
+  }, [commandPermission]);
+
+  const updateChannelsListState = useCallback((e) => {
+    const { target } = e;
+    const { name: commandName, value } = target;
+    // update state
+    setEditingCommandPermission((commandPermissionObj) => {
+      return {
+        ...getUpdatedChannelsList(commandPermissionObj, commandName, value),
+      };
+    });
+  }, []);
+
+  const updateCommandsHandler = async(e) => {
+    try {
+      await apiv3Put('/slack-integration-settings/without-proxy/update-permissions', {
+        commandPermission: editingCommandPermission,
+      });
+      toastSuccess(t('toaster.update_successed', { target: 'Token' }));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  };
+
+  return (
+    <div className="py-4 px-5">
+      <p className="mb-4 font-weight-bold">{t('admin:slack_integration.accordion.manage_commands')}</p>
+      <div className="row d-flex flex-column align-items-center">
+        <div className="col-8">
+          <div className="custom-control custom-checkbox">
+            <div className="row mb-5 d-block">
+              { defaultCommandsName.map((commandName) => {
+                // eslint-disable-next-line max-len
+                return (
+                  <PermissionSettingForEachCommandComponent
+                    key={`${commandName}-component`}
+                    commandName={commandName}
+                    editingCommandPermission={editingCommandPermission}
+                    onPermissionTypeClicked={updatePermissionsCommandsState}
+                    onPermissionListChanged={updateChannelsListState}
+                  />
+                );
+              })}
+            </div>
+          </div>
+        </div>
+      </div>
+      <div className="row">
+        <button
+          type="submit"
+          className="btn btn-primary mx-auto"
+          onClick={updateCommandsHandler}
+        >
+          { t('Update') }
+        </button>
+      </div>
+    </div>
+  );
+};
+
+ManageCommandsProcessWithoutProxy.propTypes = {
+  apiv3Put: PropTypes.func,
+  commandPermission: PropTypes.object,
+};
+
+export default ManageCommandsProcessWithoutProxy;

+ 4 - 1
packages/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -27,6 +27,7 @@ const SlackIntegration = (props) => {
   const [slackBotToken, setSlackBotToken] = useState(null);
   const [slackSigningSecretEnv, setSlackSigningSecretEnv] = useState('');
   const [slackBotTokenEnv, setSlackBotTokenEnv] = useState('');
+  const [commandPermission, setCommandPermission] = useState(null);
   const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState(false);
   const [slackAppIntegrations, setSlackAppIntegrations] = useState();
   const [proxyServerUri, setProxyServerUri] = useState();
@@ -40,7 +41,7 @@ const SlackIntegration = (props) => {
     try {
       const { data } = await appContainer.apiv3.get('/slack-integration-settings');
       const {
-        slackSigningSecret, slackBotToken, slackSigningSecretEnvVars, slackBotTokenEnvVars, slackAppIntegrations, proxyServerUri,
+        slackSigningSecret, slackBotToken, slackSigningSecretEnvVars, slackBotTokenEnvVars, slackAppIntegrations, proxyServerUri, commandPermission,
       } = data.settings;
 
       setErrorMsg(data.errorMsg);
@@ -53,6 +54,7 @@ const SlackIntegration = (props) => {
       setSlackBotTokenEnv(slackBotTokenEnvVars);
       setSlackAppIntegrations(slackAppIntegrations);
       setProxyServerUri(proxyServerUri);
+      setCommandPermission(commandPermission);
     }
     catch (err) {
       toastError(err);
@@ -151,6 +153,7 @@ const SlackIntegration = (props) => {
           onTestConnectionInvoked={fetchSlackIntegrationData}
           onUpdatedSecretToken={changeSecretAndToken}
           connectionStatuses={connectionStatuses}
+          commandPermission={commandPermission}
         />
       );
       break;

+ 4 - 5
packages/app/src/server/routes/apiv3/slack-integration-settings.js

@@ -106,6 +106,7 @@ module.exports = (crowi) => {
       'slackbot:withoutProxy:signingSecret': null,
       'slackbot:withoutProxy:botToken': null,
       'slackbot:proxyUri': null,
+      'slackbot:withoutProxy:commandPermission': null,
     };
 
     // set url if officialBot is specified
@@ -372,7 +373,7 @@ module.exports = (crowi) => {
         slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:withoutProxy:signingSecret'),
         slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:withoutProxy:botToken'),
       };
-      return res.apiv3({ customBotWithoutProxySettingParams });
+      return res.apiv3();
     }
     catch (error) {
       const msg = 'Error occured in updating Custom bot setting';
@@ -404,14 +405,12 @@ module.exports = (crowi) => {
 
     const { commandPermission } = req.body;
     const requestParams = {
-      'slackbot:withoutProxy:commandPermission': commandPermission,
+      'slackbot:withoutProxy:commandPermission': JSON.stringify(commandPermission),
     };
     try {
       await updateSlackBotSettings(requestParams);
       crowi.slackIntegrationService.publishUpdatedMessage();
-
-      const customBotWithoutProxyCommandPermissionarams = crowi.configManager.getConfig('crowi', 'slackbot:withoutProxy:commandPermission');
-      return res.apiv3({ customBotWithoutProxyCommandPermissionarams });
+      return res.apiv3();
     }
     catch (error) {
       const msg = 'Error occured in updating command permission settigns';