Просмотр исходного кода

Merge remote-tracking branch 'origin/feat/growi-bot' into feat/growi-bot-proxy

Yuki Takei 5 лет назад
Родитель
Сommit
2cb0a8434a

+ 1 - 0
resource/locales/en_US/admin/admin.json

@@ -253,6 +253,7 @@
     "delete": "Delete"
   },
   "slack_integration": {
+    "bot_reset_successful": "Bot settings have been reset.",
     "modal": {
       "warning": "Warning",
       "sure_change_bot_type": "Are you sure you want to change the bot type?",

+ 1 - 0
resource/locales/ja_JP/admin/admin.json

@@ -251,6 +251,7 @@
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
   },
   "slack_integration": {
+    "bot_reset_successful": "Botの設定を消去しました。",
     "modal": {
       "warning": "注意",
       "sure_change_bot_type": "Botの種類を変更しますか?",

+ 1 - 0
resource/locales/zh_CN/admin/admin.json

@@ -261,6 +261,7 @@
 		"delete": "删除"
   },
   "slack_integration": {
+    "bot_reset_successful": "删除了BOT设置。",
     "modal": {
       "warning": "警告",
       "sure_change_bot_type": "您确定要更改设置吗?",

+ 34 - 5
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx

@@ -2,21 +2,40 @@ import React, { useState, useEffect, useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 import PropTypes from 'prop-types';
 import AppContainer from '../../../services/AppContainer';
+import AdminAppContainer from '../../../services/AdminAppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+import SlackGrowiBridging from './SlackGrowiBridging';
+
 
 const CustomBotWithoutProxySettings = (props) => {
-  const { appContainer } = props;
+  const { appContainer, adminAppContainer } = props;
   const { t } = useTranslation();
 
   const [slackSigningSecret, setSlackSigningSecret] = useState('');
   const [slackBotToken, setSlackBotToken] = useState('');
   const [slackSigningSecretEnv, setSlackSigningSecretEnv] = useState('');
   const [slackBotTokenEnv, setSlackBotTokenEnv] = useState('');
-  const botType = 'custom-bot-without-proxy';
+  const [slackWSNameInWithoutProxy, setSlackWSNameInWithoutProxy] = useState(null);
+  // get site name from this GROWI
+  // eslint-disable-next-line no-unused-vars
+  const [siteName, setSiteName] = useState('');
+  const currentBotType = 'custom-bot-without-proxy';
+
+  const getSlackWSInWithoutProxy = useCallback(async() => {
+    try {
+      const res = await appContainer.apiv3.get('/slack-integration/custom-bot-without-proxy/slack-workspace-name');
+      setSlackWSNameInWithoutProxy(res.data.slackWorkSpaceName);
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [appContainer]);
+
   const fetchData = useCallback(async() => {
     try {
+      await adminAppContainer.retrieveAppSettingsData();
       const res = await appContainer.apiv3.get('/slack-integration/');
       const {
         slackSigningSecret, slackBotToken, slackSigningSecretEnvVars, slackBotTokenEnvVars,
@@ -25,11 +44,12 @@ const CustomBotWithoutProxySettings = (props) => {
       setSlackBotToken(slackBotToken);
       setSlackSigningSecretEnv(slackSigningSecretEnvVars);
       setSlackBotTokenEnv(slackBotTokenEnvVars);
+      setSiteName(adminAppContainer.state.title);
     }
     catch (err) {
       toastError(err);
     }
-  }, [appContainer]);
+  }, [appContainer, adminAppContainer]);
 
   useEffect(() => {
     fetchData();
@@ -40,8 +60,9 @@ const CustomBotWithoutProxySettings = (props) => {
       await appContainer.apiv3.put('/slack-integration/custom-bot-without-proxy', {
         slackSigningSecret,
         slackBotToken,
-        botType,
+        currentBotType,
       });
+      getSlackWSInWithoutProxy();
       toastSuccess(t('toaster.update_successed', { target: t('admin:slack_integration.custom_bot_without_proxy_settings') }));
     }
     catch (err) {
@@ -52,6 +73,11 @@ const CustomBotWithoutProxySettings = (props) => {
   return (
     <>
       <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_without_proxy_settings')}</h2>
+      {/* temporarily put bellow component */}
+      <SlackGrowiBridging
+        siteName={siteName}
+        slackWorkSpaceName={slackWSNameInWithoutProxy}
+      />
       <div className="row my-5">
         <div className="mx-auto">
           <button
@@ -91,6 +117,7 @@ const CustomBotWithoutProxySettings = (props) => {
                 readOnly
               />
               <p className="form-text text-muted">
+                {/* eslint-disable-next-line react/no-danger */}
                 <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACK_SIGNING_SECRET' }) }} />
               </p>
             </td>
@@ -113,6 +140,7 @@ const CustomBotWithoutProxySettings = (props) => {
                 readOnly
               />
               <p className="form-text text-muted">
+                {/* eslint-disable-next-line react/no-danger */}
                 <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACK_BOT_TOKEN' }) }} />
               </p>
             </td>
@@ -127,10 +155,11 @@ const CustomBotWithoutProxySettings = (props) => {
   );
 };
 
-const CustomBotWithoutProxySettingsWrapper = withUnstatedContainers(CustomBotWithoutProxySettings, [AppContainer]);
+const CustomBotWithoutProxySettingsWrapper = withUnstatedContainers(CustomBotWithoutProxySettings, [AppContainer, AdminAppContainer]);
 
 CustomBotWithoutProxySettings.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
 };
 
 export default CustomBotWithoutProxySettingsWrapper;

+ 17 - 0
src/client/js/components/Admin/SlackIntegration/SlackGrowiBridging.jsx

@@ -0,0 +1,17 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const SlackGrowiBridging = (props) => {
+  return (
+    <>
+      {props.slackWorkSpaceName}{props.siteName}
+    </>
+  );
+};
+
+SlackGrowiBridging.propTypes = {
+  slackWorkSpaceName: PropTypes.string,
+  siteName: PropTypes.string,
+};
+
+export default SlackGrowiBridging;

+ 50 - 10
src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -1,15 +1,37 @@
-import React, { useState } from 'react';
-
+import React, { useState, useEffect, useCallback } from 'react';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+import AppContainer from '../../../services/AppContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
 import AccessTokenSettings from './AccessTokenSettings';
 import OfficialBotSettings from './OfficialBotSettings';
 import CustomBotWithoutProxySettings from './CustomBotWithoutProxySettings';
 import CustomBotWithProxySettings from './CustomBotWithProxySettings';
 import ConfirmBotChangeModal from './ConfirmBotChangeModal';
 
-const SlackIntegration = () => {
+
+const SlackIntegration = (props) => {
+  const { appContainer } = props;
+  const { t } = useTranslation();
   const [currentBotType, setCurrentBotType] = useState(null);
   const [selectedBotType, setSelectedBotType] = useState(null);
 
+  const fetchData = useCallback(async() => {
+    try {
+      const response = await appContainer.apiv3.get('slack-integration/');
+      const { currentBotType } = response.data.slackBotSettingParams;
+      setCurrentBotType(currentBotType);
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [appContainer.apiv3]);
+
+  useEffect(() => {
+    fetchData();
+  }, [fetchData]);
+
   const handleBotTypeSelect = (clickedBotType) => {
     if (clickedBotType === currentBotType) {
       return;
@@ -25,9 +47,20 @@ const SlackIntegration = () => {
     setSelectedBotType(null);
   };
 
-  const changeCurrentBotSettings = () => {
-    setCurrentBotType(selectedBotType);
-    setSelectedBotType(null);
+  const handleChangeCurrentBotSettings = async() => {
+    try {
+      const res = await appContainer.apiv3.put('slack-integration/custom-bot-without-proxy', {
+        slackSigningSecret: '',
+        slackBotToken: '',
+        currentBotType: selectedBotType,
+      });
+      setCurrentBotType(res.data.customBotWithoutProxySettingParams.slackBotType);
+      setSelectedBotType(null);
+      toastSuccess(t('admin:slack_integration.bot_reset_successful'));
+    }
+    catch (err) {
+      toastError(err);
+    }
   };
 
   let settingsComponent = null;
@@ -37,7 +70,9 @@ const SlackIntegration = () => {
       settingsComponent = <OfficialBotSettings />;
       break;
     case 'custom-bot-without-proxy':
-      settingsComponent = <CustomBotWithoutProxySettings />;
+      settingsComponent = (
+        <CustomBotWithoutProxySettings />
+      );
       break;
     case 'custom-bot-with-proxy':
       settingsComponent = <CustomBotWithProxySettings />;
@@ -49,7 +84,7 @@ const SlackIntegration = () => {
       <div className="container">
         <ConfirmBotChangeModal
           isOpen={selectedBotType != null}
-          onConfirmClick={changeCurrentBotSettings}
+          onConfirmClick={handleChangeCurrentBotSettings}
           onCancelClick={handleCancelBotChange}
         />
       </div>
@@ -61,7 +96,6 @@ const SlackIntegration = () => {
         </div>
       </div>
 
-
       <div className="row my-5">
         <div className="card-deck mx-auto">
 
@@ -103,4 +137,10 @@ const SlackIntegration = () => {
   );
 };
 
-export default SlackIntegration;
+const SlackIntegrationWrapper = withUnstatedContainers(SlackIntegration, [AppContainer]);
+
+SlackIntegration.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
+
+export default SlackIntegrationWrapper;

+ 138 - 14
src/server/routes/apiv3/slack-integration.js

@@ -3,6 +3,7 @@ const loggerFactory = require('@alias/logger');
 const logger = loggerFactory('growi:routes:apiv3:notification-setting');
 const express = require('express');
 const { body } = require('express-validator');
+const { WebClient } = require('@slack/web-api');
 const crypto = require('crypto');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
@@ -27,7 +28,13 @@ const router = express.Router();
  *            type: string
  *          slackBotToken:
  *            type: string
- *          botType:
+ *          currentBotType:
+ *            type: string
+ *      SlackIntegration:
+ *        description: SlackIntegration
+ *        type: object
+ *        properties:
+ *          currentBotType:
  *            type: string
  */
 
@@ -39,12 +46,15 @@ module.exports = (crowi) => {
   const csrf = require('../../middlewares/csrf')(crowi);
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
 
-
   const validator = {
-    CusotmBotWithoutProxy: [
+    CustomBotWithoutProxy: [
       body('slackSigningSecret').isString(),
       body('slackBotToken').isString(),
-      body('botType').isString(),
+      body('currentBotType').isString(),
+    ],
+    SlackIntegration: [
+      body('currentBotType')
+        .isIn(['official-bot', 'custom-bot-without-proxy', 'custom-bot-with-proxy']),
     ],
   };
 
@@ -69,16 +79,15 @@ module.exports = (crowi) => {
    *      get:
    *        tags: [SlackBotSettingParams]
    *        operationId: getSlackBotSettingParams
-   *        summary: /slack-integration
+   *        summary: get /slack-integration
    *        description: Get slackBot setting params.
    *        responses:
    *          200:
    *            description: Succeeded to get slackBot setting params.
    */
   router.get('/', accessTokenParser, loginRequiredStrictly, adminRequired, async(req, res) => {
-
     const slackBotSettingParams = {
-      slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:type'),
+      currentBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
       // TODO impl when creating official bot
       officialBotSettings: {
         // TODO impl this after GW-4939
@@ -101,6 +110,52 @@ module.exports = (crowi) => {
     return res.apiv3({ slackBotSettingParams });
   });
 
+  /**
+   * @swagger
+   *
+   *    /slack-integration/:
+   *      put:
+   *        tags: [SlackIntegration]
+   *        operationId: putSlackIntegration
+   *        summary: put /slack-integration
+   *        description: Put SlackIntegration setting.
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/SlackIntegration'
+   *        responses:
+   *           200:
+   *             description: Succeeded to put Slack Integration setting.
+   */
+  router.put('/',
+    accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.SlackIntegration, apiV3FormValidator, async(req, res) => {
+      const { currentBotType } = req.body;
+
+      const requestParams = {
+        'slackbot:currentBotType': currentBotType,
+      };
+
+      try {
+        await updateSlackBotSettings(requestParams);
+
+        // initialize bolt service
+        crowi.boltService.initialize();
+        crowi.boltService.publishUpdatedMessage();
+
+        const slackIntegrationSettingsParams = {
+          currentBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
+        };
+        return res.apiv3({ slackIntegrationSettingsParams });
+      }
+      catch (error) {
+        const msg = 'Error occured in updating Slack bot setting';
+        logger.error('Error', error);
+        return res.apiv3Err(new ErrorV3(msg, 'update-SlackIntegrationSetting-failed'));
+      }
+    });
+
   /**
    * @swagger
    *
@@ -121,13 +176,13 @@ module.exports = (crowi) => {
    *             description: Succeeded to put CustomBotWithoutProxy setting.
    */
   router.put('/custom-bot-without-proxy',
-    accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.CusotmBotWithoutProxy, apiV3FormValidator, async(req, res) => {
-      const { slackSigningSecret, slackBotToken, botType } = req.body;
+    accessTokenParser, loginRequiredStrictly, adminRequired, csrf, validator.CustomBotWithoutProxy, apiV3FormValidator, async(req, res) => {
+      const { slackSigningSecret, slackBotToken, currentBotType } = req.body;
 
       const requestParams = {
         'slackbot:signingSecret': slackSigningSecret,
         'slackbot:token': slackBotToken,
-        'slackbot:type': botType,
+        'slackbot:currentBotType': currentBotType,
       };
 
       try {
@@ -141,17 +196,51 @@ module.exports = (crowi) => {
         const customBotWithoutProxySettingParams = {
           slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
           slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
-          slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:type'),
+          slackBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
         };
         return res.apiv3({ customBotWithoutProxySettingParams });
       }
       catch (error) {
         const msg = 'Error occured in updating Custom bot setting';
         logger.error('Error', error);
-        return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'));
+        return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
       }
     });
 
+  /**
+   * @swagger
+   *
+   *    /slack-integration/custom-bot-without-proxy/slack-workspace-name:
+   *      get:
+   *        tags: [slackWorkSpaceName]
+   *        operationId: getSlackWorkSpaceName
+   *        summary: Get slack work space name for custom bot without proxy
+   *        description: get slack WS name in custom bot without proxy
+   *        responses:
+   *          200:
+   *            description: Succeeded to get slack ws name for custom bot without proxy
+   */
+  router.get('/custom-bot-without-proxy/slack-workspace-name', async(req, res) => {
+    // get ws name in custom bot from slackbot token
+    const slackBotToken = crowi.configManager.getConfig('crowi', 'slackbot:token');
+
+    let slackWorkSpaceName = null;
+    if (slackBotToken != null) {
+      const web = new WebClient(slackBotToken);
+      try {
+        const slackTeamInfo = await web.team.info();
+        slackWorkSpaceName = slackTeamInfo.team.name;
+      }
+      catch (error) {
+        const msg = 'Error occured in slack_bot_token';
+        logger.error('Error', msg);
+        return res.apiv3Err(new ErrorV3(msg, 'get-SlackWorkSpaceName-failed'));
+      }
+    }
+
+    return res.apiv3({ slackWorkSpaceName });
+  });
+
   /**
    * @swagger
    *
@@ -165,18 +254,53 @@ module.exports = (crowi) => {
    *          200:
    *            description: Succeeded to update access token for slack
    */
-  router.put('/access-token', loginRequiredStrictly, adminRequired, async(req, res) => {
+  router.put('/access-token', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
 
     try {
       const accessToken = generateAccessToken(req.user);
       await updateSlackBotSettings({ 'slackbot:access-token': accessToken });
 
+      // initialize bolt service
+      crowi.boltService.initialize();
+      crowi.boltService.publishUpdatedMessage();
+
       return res.apiv3({ accessToken });
     }
     catch (error) {
       const msg = 'Error occured in updating access token for access token';
       logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(msg, 'update-accessToken-failed'));
+      return res.apiv3Err(new ErrorV3(msg, 'update-accessToken-failed'), 500);
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /slack-integration/access-token:
+   *      delete:
+   *        tags: [SlackIntegration]
+   *        operationId: deleteAccessTokenForSlackBot
+   *        summary: /slack-integration
+   *        description: Delete accessToken
+   *        responses:
+   *          200:
+   *            description: Succeeded to delete accessToken
+   */
+  router.delete('/access-token', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+
+    try {
+      await updateSlackBotSettings({ 'slackbot:access-token': null });
+
+      // initialize bolt service
+      crowi.boltService.initialize();
+      crowi.boltService.publishUpdatedMessage();
+
+      return res.apiv3({});
+    }
+    catch (error) {
+      const msg = 'Error occured in discard of slackbotAccessToken';
+      logger.error('Error', error);
+      return res.apiv3Err(new ErrorV3(msg, 'discard-slackbotAccessToken-failed'), 500);
     }
   });
 

+ 1 - 1
src/server/service/config-loader.js

@@ -412,7 +412,7 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
   },
   SLACK_BOT_TYPE: {
     ns:      'crowi',
-    key:     'slackbot:type', // eg. official || custom-without-proxy || custom-with-proxy
+    key:     'slackbot:currentBotType', // 'official-bot' || 'custom-bot-without-proxy' || 'custom-bot-with-proxy'
     type:    TYPES.STRING,
     default: null,
   },