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

Merge branch 'feat/growi-bot' into fix/GW-6084-fix-appearance-for-with-proxy

Shun Miyazawa пре 4 година
родитељ
комит
715ba7c0eb

+ 5 - 3
.github/workflows/release-slackbot.yml → .github/workflows/release-slackbot-proxy.yml

@@ -1,9 +1,9 @@
-name: Release Docker Image for growi-slackbot
+name: Release Docker Image for @growi/slackbot-proxy
 
 on:
   push:
     branches:
-      - release/slackbot/**
+      - release/slackbot-proxy/**
 
 jobs:
 
@@ -35,9 +35,10 @@ jobs:
 
     - name: Build and push
       uses: docker/build-push-action@v2
+      working-directory: ./packages/slackbot-proxy
       with:
         context: .
-        file: ./packages/slackbot-proxy/docker/Dockerfile
+        file: ./docker/Dockerfile
         platforms: linux/amd64
         push: true
         tags: |
@@ -48,6 +49,7 @@ jobs:
 
     - name: Update Docker Hub Description
       uses: peter-evans/dockerhub-description@v2
+      working-directory: ./packages/slackbot-proxy
       with:
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}

+ 24 - 3
packages/slack/src/utils/check-communicable.ts

@@ -33,12 +33,24 @@ export const connectToSlackApiServer = async(): Promise<void|AxiosError> => {
  * Test Slack API
  * @param client
  */
-const testSlackApiServer = async(client: WebClient): Promise<void> => {
+const testSlackApiServer = async(client: WebClient): Promise<any> => {
   const result = await client.api.test();
 
   if (!result.ok) {
     throw new Error(result.error);
   }
+
+  return result;
+};
+
+const checkSlackScopes = (resultTestSlackApiServer: any) => {
+  const slackScopes = resultTestSlackApiServer.response_metadata.scopes;
+  const correctScopes = ['commands', 'team:read', 'chat:write'];
+  const isPassedScopeCheck = correctScopes.every(e => slackScopes.includes(e));
+
+  if (!isPassedScopeCheck) {
+    throw new Error('The scopes is not appropriate. Required scopes is [\'commands\', \'team:read\', \'chat:write\']');
+  }
 };
 
 /**
@@ -94,7 +106,16 @@ export const getConnectionStatuses = async(tokens: string[]): Promise<{[key: str
  * @param token bot OAuth token
  * @returns
  */
-export const relationTestToSlack = async(token:string): Promise<void> => {
+export const testToSlack = async(token:string): Promise<void> => {
+  const client = generateWebClient(token);
+  const res = await testSlackApiServer(client);
+  await checkSlackScopes(res);
+};
+
+export const sendSuccessMessage = async(token:string, channel:string, appSiteUrl:string): Promise<void> => {
   const client = generateWebClient(token);
-  await testSlackApiServer(client);
+  await client.chat.postMessage({
+    channel,
+    text: `Successfully tested with ${appSiteUrl}.`,
+  });
 };

+ 2 - 2
packages/slack/src/utils/webclient-factory.ts

@@ -7,6 +7,6 @@ const isProduction = process.env.NODE_ENV === 'production';
  * @param token Slack Bot Token or Proxy Server URI
  * @returns
  */
-export const generateWebClient = (token: string, serverUri?: string): WebClient => {
-  return new WebClient(token, { slackApiUrl: serverUri, logLevel: isProduction ? LogLevel.DEBUG : LogLevel.INFO });
+export const generateWebClient = (token: string, serverUri?: string, headers?:{[key:string]:string}): WebClient => {
+  return new WebClient(token, { slackApiUrl: serverUri, logLevel: isProduction ? LogLevel.DEBUG : LogLevel.INFO, headers });
 };

+ 6 - 5
packages/slackbot-proxy/docker/Dockerfile

@@ -33,7 +33,7 @@ RUN npx lerna bootstrap
 FROM deps-resolver-base AS deps-resolver-prod
 RUN npx lerna bootstrap -- --production
 # make artifacts
-RUN tar cf node_modules.tar node_modules
+RUN tar cf dependencies.tar node_modules packages/slackbot-proxy/node_modules
 
 
 ##
@@ -79,8 +79,9 @@ ENV NODE_ENV production
 
 ENV appDir /opt
 
+# copy artifacts
 COPY --from=deps-resolver-prod --chown=node:node \
-  ${appDir}/node_modules.tar ${appDir}/
+  ${appDir}/dependencies.tar ${appDir}/
 COPY --from=builder --chown=node:node \
   ${appDir}/packages.tar ${appDir}/
 
@@ -88,11 +89,11 @@ RUN chown node:node ${appDir}
 
 USER node
 
-# extract node_modules.tar
+# extract artifacts
 WORKDIR ${appDir}
-RUN tar xf node_modules.tar
+RUN tar xf dependencies.tar
 RUN tar xf packages.tar
-RUN rm node_modules.tar packages.tar
+RUN rm dependencies.tar packages.tar
 
 WORKDIR ${appDir}/packages/slackbot-proxy
 

+ 66 - 7
packages/slackbot-proxy/src/controllers/growi-to-slack.ts

@@ -1,11 +1,13 @@
 import {
-  Controller, Get, Inject, Req, Res, UseBefore,
+  Controller, Get, Post, Inject, Req, Res, UseBefore,
 } from '@tsed/common';
 import axios from 'axios';
 
-import { WebAPICallResult } from '@slack/web-api';
+import { WebAPICallOptions, WebAPICallResult } from '@slack/web-api';
 
-import { verifyGrowiToSlackRequest, getConnectionStatuses, relationTestToSlack } from '@growi/slack';
+import {
+  verifyGrowiToSlackRequest, getConnectionStatuses, testToSlack, generateWebClient,
+} from '@growi/slack';
 
 import { GrowiReq } from '~/interfaces/growi-to-slack/growi-req';
 import { InstallationRepository } from '~/repositories/installation';
@@ -105,8 +107,15 @@ export class GrowiToSlackCtrl {
         return res.status(400).send({ message: `failed to request to GROWI. err: ${err.message}` });
       }
 
-      await relationTestToSlack(token);
-      return res.send({ relation });
+      try {
+        await testToSlack(token);
+      }
+      catch (err) {
+        logger.error(err);
+        return res.status(400).send({ message: `failed to test. err: ${err.message}` });
+      }
+
+      return res.send({ relation, slackBotToken: token });
     }
 
     // retrieve latest Order with Installation
@@ -136,7 +145,13 @@ export class GrowiToSlackCtrl {
       return res.status(400).send({ message: 'installation is invalid' });
     }
 
-    await relationTestToSlack(token);
+    try {
+      await testToSlack(token);
+    }
+    catch (err) {
+      logger.error(err);
+      return res.status(400).send({ message: `failed to test. err: ${err.message}` });
+    }
 
     logger.debug('relation test is success', order);
 
@@ -145,7 +160,51 @@ export class GrowiToSlackCtrl {
       installation: order.installation, tokenGtoP: order.tokenGtoP, tokenPtoG: order.tokenPtoG, growiUri: order.growiUrl,
     });
 
-    return res.send({ relation: createdRelation });
+    return res.send({ relation: createdRelation, slackBotToken: token });
+  }
+
+  @Post('/*')
+  @UseBefore(verifyGrowiToSlackRequest)
+  async postResult(@Req() req: GrowiReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
+    const { tokenGtoPs } = req;
+
+    if (tokenGtoPs.length !== 1) {
+      return res.status(400).send({ message: 'tokenGtoPs is invalid' });
+    }
+
+    const tokenGtoP = tokenGtoPs[0];
+
+    // retrieve relation with Installation
+    const relation = await this.relationRepository.createQueryBuilder('relation')
+      .where('tokenGtoP = :token', { token: tokenGtoP })
+      .leftJoinAndSelect('relation.installation', 'installation')
+      .getOne();
+
+    if (relation == null) {
+      return res.status(400).send({ message: 'relation is invalid' });
+    }
+
+    const token = relation.installation.data.bot?.token;
+    if (token == null) {
+      return res.status(400).send({ message: 'installation is invalid' });
+    }
+
+    const client = generateWebClient(token);
+
+    try {
+      // TODO: GW-6133
+      const opt = req.body as WebAPICallOptions;
+      await client.apiCall('put', opt);
+    }
+    catch (err) {
+      // TODO: GW-6133
+      // logger.error()
+      return res.status(500).send({ message: err.message });
+    }
+
+    logger.debug('postMessage is success');
+
+    return res.end();
   }
 
 }

+ 3 - 2
resource/locales/en_US/admin/admin.json

@@ -279,6 +279,7 @@
       "impossible": "Impossible"
     },
     "bot_reset_successful": "Bot settings have been reset.",
+    "adding_slack_ws_integration_settings_successful": "Slack workspace integration settings have been added",
     "bot_all_reset_successful": "All Bot settings have been reset.",
     "copied_to_clipboard": "Copied to clipboard",
     "set_scope": "Please set up Bot Token Scopes from Slack settings",
@@ -291,12 +292,12 @@
     },
     "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <code>{{variable}}</code> is used.",
     "access_token_settings": {
-      "discard": "Discard",
-      "generate": "Generate"
+      "regenerate": "Regenerate"
     },
     "delete": "Delete",
     "integration_procedure": "Integration Procedure",
     "custom_bot_without_proxy_settings": "Custom Bot without proxy Settings",
+    "integration_failed":"Integration failed",
     "official_bot_settings": "Official bot Settings",
     "reset": "Reset",
     "reset_all_settings": "Reset all settings",

+ 3 - 2
resource/locales/ja_JP/admin/admin.json

@@ -277,6 +277,7 @@
       "impossible": "不可"
     },
     "bot_reset_successful": "Botの設定を消去しました。",
+    "adding_slack_ws_integration_settings_successful": "Slack ワークスペースとの連携設定を追加しました。",
     "bot_all_reset_successful": "全ての Bot の設定を消去しました。",
     "copied_to_clipboard": "クリップボードにコピーされました。",
     "set_scope": "Slackの設定画面からBot Token Scopeを設定してください",
@@ -289,12 +290,12 @@
     },
     "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します",
     "access_token_settings": {
-      "discard": "破棄",
-      "generate": "発行"
+      "regenerate": "再発行"
     },
     "delete": "削除",
     "integration_procedure": "連携手順",
     "custom_bot_without_proxy_settings": "Custom Bot (Without-Proxy) 設定",
+    "integration_failed":"連携に失敗しました",
     "reset": "リセット",
     "reset_all_settings": "全ての設定をリセット",
     "delete_slackbot_settings": "Slack Bot 設定をリセットする",

+ 3 - 2
resource/locales/zh_CN/admin/admin.json

@@ -287,6 +287,7 @@
       "impossible": "不可能"
     },
     "bot_reset_successful": "删除了BOT设置。",
+    "adding_slack_ws_integration_settings_successful": "添加了用于与 Slack 工作空间链接的设置。",
     "bot_all_reset_successful": "所有的 Bot 设置都被清除了。",
     "copied_to_clipboard": "它已复制到剪贴板。",
     "set_scope": "在Slack设置页面中配置Bot Token Scope。",
@@ -299,12 +300,12 @@
     },
     "use_env_var_if_empty": "如果数据库中的值为空,则环境变量的值 <code>{{variable}}</code> 启用。",
     "access_token_settings": {
-      "discard": "丢弃",
-      "generate": "生成"
+      "regenerate": "再生"
     },
     "delete": "取消",
     "integration_procedure": "协作程序",
     "custom_bot_without_proxy_settings": "Custom Bot (Without-Proxy) 设置",
+    "integration_failed":"联动失败",
     "reset":"重置",
     "reset_all_settings": "重置所有设置",
     "delete_slackbot_settings": "重置 Slack Bot 设置",

+ 0 - 65
src/client/js/components/Admin/SlackIntegration/AccessTokenSettings.jsx

@@ -1,65 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { useTranslation } from 'react-i18next';
-import { CopyToClipboard } from 'react-copy-to-clipboard';
-import { toastSuccess } from '../../../util/apiNotification';
-
-const AccessTokenSettings = (props) => {
-  const { t } = useTranslation('admin');
-
-  const onClickDiscardButton = () => {
-    if (props.onClickDiscardButton != null) {
-      props.onClickDiscardButton();
-    }
-  };
-
-  const onClickGenerateToken = () => {
-    if (props.onClickGenerateToken != null) {
-      props.onClickGenerateToken();
-    }
-  };
-
-  const accessToken = props.accessToken ? props.accessToken : '';
-
-  return (
-    <div className="row">
-      <div className="col-lg-12">
-
-        <h2 className="admin-setting-header">Access Token</h2>
-
-        <div className="form-group row my-5">
-          <label className="text-left text-md-right col-md-3 col-form-label">Access Token</label>
-          <div className="col-md-6">
-            {accessToken.length === 0 ? (
-              <input className="form-control" type="text" value={accessToken} readOnly />
-            ) : (
-              <CopyToClipboard text={accessToken} onCopy={() => toastSuccess(t('admin:slack_integration.copied_to_clipboard'))}>
-                <input className="form-control" type="text" value={accessToken} readOnly />
-              </CopyToClipboard>
-            )}
-          </div>
-        </div>
-
-        <div className="row">
-          <div className="mx-auto">
-            <button type="button" className="btn btn-outline-secondary text-nowrap mx-1" onClick={onClickDiscardButton} disabled={accessToken.length === 0}>
-              {t('slack_integration.access_token_settings.discard')}
-            </button>
-            <button type="button" className="btn btn-primary text-nowrap mx-1" onClick={onClickGenerateToken}>
-              {t('slack_integration.access_token_settings.generate')}
-            </button>
-          </div>
-        </div>
-
-      </div>
-    </div>
-  );
-};
-
-AccessTokenSettings.propTypes = {
-  accessToken: PropTypes.string,
-  onClickDiscardButton: PropTypes.func,
-  onClickGenerateToken: PropTypes.func,
-};
-
-export default AccessTokenSettings;

+ 37 - 29
src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -12,11 +12,17 @@ import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 const logger = loggerFactory('growi:SlackBotSettings');
 
 const CustomBotWithProxySettings = (props) => {
-  const { appContainer, slackAppIntegrations, proxyServerUri } = props;
-  const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState(false);
+  const {
+    appContainer, slackAppIntegrations, proxyServerUri, onClickAddSlackWorkspaceBtn, connectionStatuses,
+  } = props;
+  const [newProxyServerUri, setNewProxyServerUri] = useState();
+  const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
   const { t } = useTranslation();
 
-  const [newProxyServerUri, setNewProxyServerUri] = useState();
+  const workspaceNameObjects = Object.values(connectionStatuses);
+  const workspaceNames = workspaceNameObjects.map((w) => {
+    return w.workspaceName;
+  });
 
   useEffect(() => {
     if (proxyServerUri != null) {
@@ -24,40 +30,38 @@ const CustomBotWithProxySettings = (props) => {
     }
   }, [proxyServerUri]);
 
-  const addSlackAppIntegrationHandler = async() => {
-    // TODO GW-6067 implement
+  const fetchSlackIntegrationData = () => {
+    if (props.fetchSlackIntegrationData != null) {
+      props.fetchSlackIntegrationData();
+    }
   };
 
-  const discardTokenHandler = async(tokenGtoP, tokenPtoG) => {
-    try {
-      // GW-6068 set new value after this
-      await appContainer.apiv3.delete('/slack-integration-settings/slack-app-integration', { tokenGtoP, tokenPtoG });
-    }
-    catch (err) {
-      toastError(err);
-      logger(err);
+  const addSlackAppIntegrationHandler = async() => {
+    if (onClickAddSlackWorkspaceBtn != null) {
+      onClickAddSlackWorkspaceBtn();
     }
   };
 
-  const generateTokenHandler = async() => {
+  const deleteSlackAppIntegrationHandler = async() => {
     try {
       // GW-6068 set new value after this
-      await appContainer.apiv3.put('/slack-integration-settings/access-tokens');
+      await appContainer.apiv3.delete('/slack-integration-settings/slack-app-integration', { integrationIdToDelete });
+      fetchSlackIntegrationData();
+      toastSuccess(t('toaster.update_successed', { target: 'Token' }));
     }
     catch (err) {
       toastError(err);
-      logger(err);
+      logger.error(err);
     }
   };
 
-  const deleteSlackAppIntegrationHandler = async() => {
+  const generateAccessTokens = async() => {
     try {
-      // TODO GW-5923 delete SlackAppIntegration
-      // await appContainer.apiv3.put('/slack-integration-settings/custom-bot-with-proxy');
-      toastSuccess('success');
+      //  TODO: imprement regenerating tokens by GW-6068
     }
     catch (err) {
       toastError(err);
+      logger.error(err);
     }
   };
 
@@ -66,7 +70,7 @@ const CustomBotWithProxySettings = (props) => {
       await appContainer.apiv3.put('/slack-integration-settings/proxy-uri', {
         proxyUri: newProxyServerUri,
       });
-      toastSuccess(t('toaster.update_successed', { target: t('Proxy URL') }));
+      toastSuccess(t('toaster.update_successed', { target: 'Proxy URL' }));
     }
     catch (err) {
       toastError(err);
@@ -113,24 +117,26 @@ const CustomBotWithProxySettings = (props) => {
 
       <h2 className="admin-setting-header">{t('admin:slack_integration.integration_procedure')}</h2>
       <div className="mx-3">
-        {slackAppIntegrations.map((slackAppIntegration) => {
+        {slackAppIntegrations.map((slackAppIntegration, i) => {
           const { tokenGtoP, tokenPtoG } = slackAppIntegration;
           return (
-            <React.Fragment key={slackAppIntegration.id}>
+            <React.Fragment key={slackAppIntegration._id}>
               <div className="d-flex justify-content-end">
                 <button
                   className="my-3 btn btn-outline-danger"
                   type="button"
-                  onClick={() => setIsDeleteConfirmModalShown(true)}
+                  onClick={() => setIntegrationIdToDelete(slackAppIntegration._id)}
                 >
                   <i className="icon-trash mr-1" />
                   {t('admin:slack_integration.delete')}
                 </button>
               </div>
+              {proxyServerUri != null && workspaceNames[i] == null
+              && (<>Settings #{i + 1} <span className="text-danger">{t('admin:slack_integration.integration_failed')}</span></>)}
               <WithProxyAccordions
                 botType="customBotWithProxy"
-                discardTokenHandler={() => discardTokenHandler(tokenGtoP, tokenPtoG)}
-                generateTokenHandler={generateTokenHandler}
+                slackAppIntegrationId={slackAppIntegration._id}
+                onClickGenerateTokenBtn={generateAccessTokens}
                 tokenGtoP={tokenGtoP}
                 tokenPtoG={tokenPtoG}
               />
@@ -149,8 +155,8 @@ const CustomBotWithProxySettings = (props) => {
       </div>
       <DeleteSlackBotSettingsModal
         isResetAll={false}
-        isOpen={isDeleteConfirmModalShown}
-        onClose={() => setIsDeleteConfirmModalShown(false)}
+        isOpen={integrationIdToDelete != null}
+        onClose={() => setIntegrationIdToDelete(null)}
         onClickDeleteButton={deleteSlackAppIntegrationHandler}
       />
     </>
@@ -165,9 +171,11 @@ CustomBotWithProxySettings.defaultProps = {
 
 CustomBotWithProxySettings.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
   slackAppIntegrations: PropTypes.array,
   proxyServerUri: PropTypes.string,
+  onClickAddSlackWorkspaceBtn: PropTypes.func,
+  fetchSlackIntegrationData: PropTypes.func,
+  connectionStatuses: PropTypes.object.isRequired,
 };
 
 export default CustomBotWithProxySettingsWrapper;

+ 58 - 40
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxyIntegrationCard.jsx

@@ -4,16 +4,70 @@ import PropTypes from 'prop-types';
 
 import { UncontrolledTooltip } from 'reactstrap';
 
-const CustomBotWithoutProxyIntegrationCard = (props) => {
+const IntegrationSuccess = () => {
+  const { t } = useTranslation();
+
+  return (
+    <>
+      <div className="d-none d-lg-block">
+        <p className="text-success small mt-5">
+          <i className="fa fa-check mr-1" />
+          {t('admin:slack_integration.integration_sentence.integration_successful')}
+        </p>
+        <hr className="align-self-center admin-border-success border-success"></hr>
+      </div>
+      <div id="integration-line-for-tooltip" className="d-block d-lg-none mt-5">
+        <i className="fa fa-check mr-1 text-success" />
+        <hr className="align-self-center admin-border-success border-success"></hr>
+      </div>
+      <UncontrolledTooltip placement="top" fade={false} target="integration-line-for-tooltip">
+        <small>
+          {t('admin:slack_integration.integration_sentence.integration_successful')}
+        </small>
+      </UncontrolledTooltip>
+    </>
+  );
+};
 
+const IntegrationFailed = () => {
   const { t } = useTranslation();
 
+  return (
+    <>
+      <div className="d-none d-lg-block">
+        <p className="mt-4">
+          <small
+            className="text-danger m-0"
+          // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
+          />
+        </p>
+        <hr className="align-self-center admin-border-danger border-danger"></hr>
+
+      </div>
+      <div id="integration-line-for-tooltip" className="d-block d-lg-none mt-5">
+        <i className="icon-info text-danger" />
+        <hr className="align-self-center admin-border-danger border-danger"></hr>
+      </div>
+      <UncontrolledTooltip placement="top" fade={false} target="integration-line-for-tooltip">
+        <small
+          className="m-0"
+        // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
+        />
+      </UncontrolledTooltip>
+    </>
+  );
+};
+
+const CustomBotWithoutProxyIntegrationCard = (props) => {
+
   return (
     <div className="d-flex justify-content-center my-5 bot-integration">
       <div className="card rounded shadow border-0 w-50 admin-bot-card mb-0">
         <h5 className="card-title font-weight-bold mt-3 ml-4">Slack</h5>
         <div className="card-body p-2 w-50 mx-auto">
-          {props.slackWSNameInWithoutProxy != null && (
+          {props.isIntegrationSuccess && props.slackWSNameInWithoutProxy != null && (
             <div className="card slack-work-space-name-card">
               <div className="m-2 text-center">
                 <h5 className="font-weight-bold">{props.slackWSNameInWithoutProxy}</h5>
@@ -25,32 +79,7 @@ const CustomBotWithoutProxyIntegrationCard = (props) => {
       </div>
 
       <div className="text-center w-25">
-
-        <div className="d-none d-lg-block">
-          {/* TODO GW-5998 switching logic */}
-          {/* <p className="text-success small mt-5">
-            <i className="fa fa-check mr-1" />
-            {t('admin:slack_integration.integration_sentence.integration_successful')}
-          </p> */}
-
-          <p className="mt-4">
-            <small
-              className="text-danger m-0"
-              // eslint-disable-next-line react/no-danger
-              dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
-            />
-          </p>
-        </div>
-
-        {/* TODO GW-5998  */}
-        <div id="integration-line-for-tooltip" className="d-block d-lg-none mt-5">
-          {/* <i className="fa fa-check mr-1 text-success" /> */}
-          <i className="icon-info text-danger" />
-        </div>
-
-        {/* TODO GW-5998 */}
-        {/* <hr className="align-self-center admin-border-success border-success"></hr> */}
-        <hr className="align-self-center admin-border-danger border-danger"></hr>
+        {props.isIntegrationSuccess ? <IntegrationSuccess /> : <IntegrationFailed />}
       </div>
 
       <div className="card rounded-lg shadow border-0 w-50 admin-bot-card mb-0">
@@ -59,18 +88,6 @@ const CustomBotWithoutProxyIntegrationCard = (props) => {
           <div className="btn btn-primary">{ props.siteName }</div>
         </div>
       </div>
-
-      <UncontrolledTooltip placement="top" fade={false} target="integration-line-for-tooltip">
-        {/* TODO GW-5998 */}
-        {/* <small>
-          {t('admin:slack_integration.integration_sentence.integration_successful')}
-        </small> */}
-        <small
-          className="m-0"
-          // eslint-disable-next-line react/no-danger
-          dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
-        />
-      </UncontrolledTooltip>
     </div>
   );
 };
@@ -78,6 +95,7 @@ const CustomBotWithoutProxyIntegrationCard = (props) => {
 CustomBotWithoutProxyIntegrationCard.propTypes = {
   siteName: PropTypes.string.isRequired,
   slackWSNameInWithoutProxy: PropTypes.string,
+  isIntegrationSuccess: PropTypes.bool,
 };
 
 export default CustomBotWithoutProxyIntegrationCard;

+ 32 - 0
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx

@@ -14,6 +14,10 @@ const CustomBotWithoutProxySettings = (props) => {
 
   const [siteName, setSiteName] = useState('');
   const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState(false);
+  const [isIntegrationSuccess, setIsIntegrationSuccess] = useState(false);
+  const [connectionMessage, setConnectionMessage] = useState('');
+  const [connectionErrorCode, setConnectionErrorCode] = useState(null);
+  const [testChannel, setTestChannel] = useState('');
 
   const resetSettings = async() => {
     if (onResetSettings == null) {
@@ -22,6 +26,25 @@ const CustomBotWithoutProxySettings = (props) => {
     onResetSettings();
   };
 
+  const testConnection = async() => {
+    setConnectionErrorCode(null);
+    setConnectionMessage(null);
+    try {
+      await appContainer.apiv3.post('/slack-integration-settings/without-proxy/test', { channel: testChannel });
+      setConnectionMessage('Send the message to slack work space.');
+      setIsIntegrationSuccess(true);
+    }
+    catch (err) {
+      setConnectionErrorCode(err[0].code);
+      setConnectionMessage(err[0].message);
+      setIsIntegrationSuccess(false);
+    }
+  };
+
+  const inputTestChannelHandler = (channel) => {
+    setTestChannel(channel);
+  };
+
   useEffect(() => {
     const siteName = appContainer.config.crowi.title;
     setSiteName(siteName);
@@ -34,6 +57,7 @@ const CustomBotWithoutProxySettings = (props) => {
       <CustomBotWithoutProxyIntegrationCard
         siteName={siteName}
         slackWSNameInWithoutProxy={props.slackWSNameInWithoutProxy}
+        isIntegrationSuccess={isIntegrationSuccess}
       />
 
       <h2 className="admin-setting-header">{t('admin:slack_integration.integration_procedure')}</h2>
@@ -51,6 +75,13 @@ const CustomBotWithoutProxySettings = (props) => {
         <CustomBotWithoutProxySettingsAccordion
           {...props}
           activeStep={botInstallationStep.CREATE_BOT}
+          connectionMessage={connectionMessage}
+          connectionErrorCode={connectionErrorCode}
+          isIntegrationSuccess={isIntegrationSuccess}
+          testChannel={testChannel}
+          onTestFormSubmitted={testConnection}
+          inputTestChannelHandler={inputTestChannelHandler}
+
         />
       </div>
       <DeleteSlackBotSettingsModal
@@ -73,6 +104,7 @@ CustomBotWithoutProxySettings.propTypes = {
   slackBotToken: PropTypes.string,
   slackBotTokenEnv: PropTypes.string,
   isRgisterSlackCredentials: PropTypes.bool,
+  isIntegrationSuccess: PropTypes.bool,
   slackWSNameInWithoutProxy: PropTypes.string,
   onResetSettings: PropTypes.func,
 };

+ 31 - 47
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -16,19 +16,15 @@ export const botInstallationStep = {
 };
 
 const CustomBotWithoutProxySettingsAccordion = ({
-  appContainer, activeStep, fetchSlackIntegrationData,
-  slackSigningSecret, slackSigningSecretEnv, slackBotToken, slackBotTokenEnv,
-  isRegisterSlackCredentials, isSendTestMessage,
-  onSetSlackSigningSecret, onSetSlackBotToken, onSetIsSendTestMessage,
+  appContainer, activeStep,
+  connectionMessage, connectionErrorCode, testChannel, slackSigningSecret, slackSigningSecretEnv, slackBotToken, slackBotTokenEnv,
+  isRegisterSlackCredentials, isIntegrationSuccess,
+  fetchSlackIntegrationData, inputTestChannelHandler, onTestFormSubmitted, onSetSlackSigningSecret, onSetSlackBotToken,
 }) => {
   const { t } = useTranslation();
   // TODO: GW-5644 Store default open accordion
   // eslint-disable-next-line no-unused-vars
   const [defaultOpenAccordionKeys, setDefaultOpenAccordionKeys] = useState(new Set([activeStep]));
-  const [connectionErrorCode, setConnectionErrorCode] = useState(null);
-  const [connectionErrorMessage, setConnectionErrorMessage] = useState(null);
-  const [connectionSuccessMessage, setConnectionSuccessMessage] = useState(null);
-  const [testChannel, setTestChannel] = useState('');
   const currentBotType = 'customBotWithoutProxy';
 
 
@@ -63,42 +59,22 @@ const CustomBotWithoutProxySettingsAccordion = ({
     }
   };
 
-  const testConnection = async() => {
-    setConnectionErrorCode(null);
-    setConnectionErrorMessage(null);
-    setConnectionSuccessMessage(null);
-    // TODO: 5921 Add new Test endpoint
-    try {
-      // eslint-disable-next-line no-console
-      console.log('Test');
-      // const res = await appContainer.apiv3.post('/slack-integration-settings//without-proxy/test', {
-      //   channel: testChannel,
-      // });
-      // setConnectionSuccessMessage(res.data.message);
-      // onSetIsSendTestMessage(true);
-    }
-    catch (err) {
-      onSetIsSendTestMessage(false);
-      setConnectionErrorCode('dummy-error-code');
-      setConnectionErrorMessage('This is a sample error message');
-    }
-  };
-
   const submitForm = (e) => {
     e.preventDefault();
-    testConnection();
-  };
 
-  const inputTestChannelHandler = (channel) => {
-    setTestChannel(channel);
+    if (onTestFormSubmitted == null) {
+      return;
+    }
+    onTestFormSubmitted();
   };
 
+
   let value = '';
-  if (connectionErrorMessage != null) {
-    value = [connectionErrorCode, connectionErrorMessage];
+  if (connectionMessage === 'Send the message to slack work space.' || connectionMessage === '') {
+    value = connectionMessage;
   }
-  if (connectionSuccessMessage != null) {
-    value = connectionSuccessMessage;
+  else {
+    value = [connectionErrorCode, connectionMessage];
   }
 
   return (
@@ -159,7 +135,7 @@ const CustomBotWithoutProxySettingsAccordion = ({
       <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')}{isSendTestMessage && <i className="ml-3 text-success fa fa-check"></i>}</>}
+        title={<><span className="mr-2">④</span>{t('admin:slack_integration.accordion.test_connection')}{isIntegrationSuccess && <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">
@@ -184,10 +160,17 @@ const CustomBotWithoutProxySettingsAccordion = ({
             </button>
           </form>
         </div>
-        {connectionErrorMessage != null
-          && <p className="text-danger text-center my-4">{t('admin:slack_integration.accordion.error_check_logs_below')}</p>}
-        {connectionSuccessMessage != null
-          && <p className="text-info text-center my-4">{t('admin:slack_integration.accordion.send_message_to_slack_work_space')}</p>}
+        {connectionMessage === ''
+          ? <p></p>
+          : (
+            <>
+              {connectionMessage === 'Send the message to slack work space.'
+                ? <p className="text-info text-center my-4">{t('admin:slack_integration.accordion.send_message_to_slack_work_space')}</p>
+                : <p className="text-danger text-center my-4">{t('admin:slack_integration.accordion.error_check_logs_below')}</p>
+              }
+            </>
+          )
+        }
         <form>
           <div className="row my-3 justify-content-center">
             <div className="form-group slack-connection-log col-md-4">
@@ -214,15 +197,16 @@ CustomBotWithoutProxySettingsAccordion.propTypes = {
   slackSigningSecretEnv: PropTypes.string,
   slackBotToken: PropTypes.string,
   slackBotTokenEnv: PropTypes.string,
+  testChannel: PropTypes.string,
   isRegisterSlackCredentials: PropTypes.bool,
-  isSendTestMessage: PropTypes.bool,
+  isIntegrationSuccess: PropTypes.bool,
   fetchSlackIntegrationData: PropTypes.func,
+  inputTestChannelHandler: PropTypes.func,
+  onTestFormSubmitted: PropTypes.func,
   onSetSlackSigningSecret: PropTypes.func,
   onSetSlackBotToken: PropTypes.func,
-  onSetIsSendTestMessage: PropTypes.func,
-  onSetIsRegisterSlackCredentials: PropTypes.func,
-  setSlackWSNameInWithoutProxy: PropTypes.func,
-
+  connectionMessage: PropTypes.string,
+  connectionErrorCode: PropTypes.string,
   adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
   activeStep: PropTypes.oneOf(Object.values(botInstallationStep)).isRequired,
 };

+ 9 - 5
src/client/js/components/Admin/SlackIntegration/OfficialBotSettings.jsx

@@ -12,7 +12,9 @@ import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 const logger = loggerFactory('growi:SlackBotSettings');
 
 const OfficialBotSettings = (props) => {
-  const { appContainer, slackAppIntegrations, proxyServerUri } = props;
+  const {
+    appContainer, slackAppIntegrations, proxyServerUri, onClickAddSlackWorkspaceBtn,
+  } = props;
   const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState(false);
   const { t } = useTranslation();
 
@@ -22,10 +24,12 @@ const OfficialBotSettings = (props) => {
     if (proxyServerUri != null) {
       setNewProxyServerUri(proxyServerUri);
     }
-  }, [proxyServerUri]);
+  }, [proxyServerUri, slackAppIntegrations]);
 
   const addSlackAppIntegrationHandler = async() => {
-    // TODO GW-6067 implement
+    if (onClickAddSlackWorkspaceBtn != null) {
+      onClickAddSlackWorkspaceBtn();
+    }
   };
 
   const discardTokenHandler = async(tokenGtoP, tokenPtoG) => {
@@ -41,8 +45,7 @@ const OfficialBotSettings = (props) => {
 
   const generateTokenHandler = async() => {
     try {
-      // GW-6068 set new value after this
-      await appContainer.apiv3.put('/slack-integration-settings/access-tokens');
+      //  TODO: imprement regenerating tokens by GW-6068
     }
     catch (err) {
       toastError(err);
@@ -169,6 +172,7 @@ OfficialBotSettings.propTypes = {
 
   slackAppIntegrations: PropTypes.array,
   proxyServerUri: PropTypes.string,
+  onClickAddSlackWorkspaceBtn: PropTypes.func,
 };
 
 export default OfficialBotSettingsWrapper;

+ 37 - 11
src/client/js/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -24,11 +24,11 @@ const SlackIntegration = (props) => {
   const [slackSigningSecretEnv, setSlackSigningSecretEnv] = useState('');
   const [slackBotTokenEnv, setSlackBotTokenEnv] = useState('');
   const [isRegisterSlackCredentials, setIsRegisterSlackCredentials] = useState(false);
-  const [isSendTestMessage, setIsSendTestMessage] = useState(false);
   const [slackWSNameInWithoutProxy, setSlackWSNameInWithoutProxy] = useState(null);
   const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState(false);
   const [slackAppIntegrations, setSlackAppIntegrations] = useState();
   const [proxyServerUri, setProxyServerUri] = useState();
+  const [connectionStatuses, setConnectionStatuses] = useState(null);
 
 
   const fetchSlackIntegrationData = useCallback(async() => {
@@ -38,12 +38,16 @@ const SlackIntegration = (props) => {
         slackSigningSecret, slackBotToken, slackSigningSecretEnvVars, slackBotTokenEnvVars, slackAppIntegrations, proxyServerUri,
       } = data.settings;
 
-      if (data.connectionStatuses != null) {
-        // TODO fix
-        // const { workspaceName } = data.connectionStatuses[slackBotToken];
-        // setSlackWSNameInWithoutProxy(workspaceName);
+      if (data.connectionStatuses == null) {
+        data.connectionStatuses = {};
       }
-
+      // if (data.connectionStatuses != null) {
+      // TODO fix
+      // const { workspaceName } = data.connectionStatuses[slackBotToken];
+      // setSlackWSNameInWithoutProxy(workspaceName);
+      // setConnectionStatuses(data.connectionStatuses);
+      // }
+      setConnectionStatuses(data.connectionStatuses);
       setCurrentBotType(data.currentBotType);
       setSlackSigningSecret(slackSigningSecret);
       setSlackBotToken(slackBotToken);
@@ -79,6 +83,17 @@ const SlackIntegration = (props) => {
     }
   };
 
+  const createSlackIntegrationData = async() => {
+    try {
+      await appContainer.apiv3.put('/slack-integration-settings/slack-app-integrations');
+      fetchSlackIntegrationData();
+      toastSuccess(t('admin:slack_integration.adding_slack_ws_integration_settings_successful'));
+    }
+    catch (error) {
+      toastError(error);
+    }
+  };
+
   useEffect(() => {
     fetchSlackIntegrationData();
   }, [fetchSlackIntegrationData]);
@@ -93,7 +108,6 @@ const SlackIntegration = (props) => {
       setIsRegisterSlackCredentials(false);
       setSlackSigningSecret(null);
       setSlackBotToken(null);
-      setIsSendTestMessage(false);
       setSlackWSNameInWithoutProxy(null);
     }
     catch (err) {
@@ -124,12 +138,17 @@ const SlackIntegration = (props) => {
 
   switch (currentBotType) {
     case 'officialBot':
-      settingsComponent = <OfficialBotSettings slackAppIntegrations={slackAppIntegrations} proxyServerUri={proxyServerUri} />;
+      settingsComponent = (
+        <OfficialBotSettings
+          slackAppIntegrations={slackAppIntegrations}
+          proxyServerUri={proxyServerUri}
+          onClickAddSlackWorkspaceBtn={createSlackIntegrationData}
+        />
+      );
       break;
     case 'customBotWithoutProxy':
       settingsComponent = (
         <CustomBotWithoutProxySettings
-          isSendTestMessage={isSendTestMessage}
           isRegisterSlackCredentials={isRegisterSlackCredentials}
           slackBotTokenEnv={slackBotTokenEnv}
           slackBotToken={slackBotToken}
@@ -138,14 +157,21 @@ const SlackIntegration = (props) => {
           slackWSNameInWithoutProxy={slackWSNameInWithoutProxy}
           onSetSlackSigningSecret={setSlackSigningSecret}
           onSetSlackBotToken={setSlackBotToken}
-          onSetIsSendTestMessage={setIsSendTestMessage}
           onResetSettings={resetWithOutSettings}
           fetchSlackIntegrationData={fetchSlackIntegrationData}
         />
       );
       break;
     case 'customBotWithProxy':
-      settingsComponent = <CustomBotWithProxySettings slackAppIntegrations={slackAppIntegrations} proxyServerUri={proxyServerUri} />;
+      settingsComponent = (
+        <CustomBotWithProxySettings
+          slackAppIntegrations={slackAppIntegrations}
+          proxyServerUri={proxyServerUri}
+          onClickAddSlackWorkspaceBtn={createSlackIntegrationData}
+          fetchSlackIntegrationData={fetchSlackIntegrationData}
+          connectionStatuses={connectionStatuses}
+        />
+      );
       break;
   }
 

+ 42 - 64
src/client/js/components/Admin/SlackIntegration/WithProxyAccordions.jsx

@@ -1,12 +1,16 @@
+/* eslint-disable react/prop-types */
 import React, { useState } from 'react';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
+import loggerFactory from '@alias/logger';
+
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess } from '../../../util/apiNotification';
 import AppContainer from '../../../services/AppContainer';
 import Accordion from '../Common/Accordion';
 
+const logger = loggerFactory('growi:SlackIntegration:WithProxyAccordionsWrapper');
 
 const BotCreateProcess = () => {
   const { t } = useTranslation();
@@ -73,15 +77,9 @@ const RegisteringProxyUrlProcess = () => {
 const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers((props) => {
   const { t } = useTranslation();
 
-  const generateTokenHandler = () => {
-    if (props.generateTokenHandler != null) {
-      props.generateTokenHandler();
-    }
-  };
-
-  const discardTokenHandler = () => {
-    if (props.discardTokenHandler != null) {
-      props.discardTokenHandler();
+  const onClickGenerateTokenBtn = () => {
+    if (props.onClickGenerateTokenBtn != null) {
+      props.onClickGenerateTokenBtn();
     }
   };
 
@@ -116,23 +114,13 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers
       </div>
 
       <div className="row my-3">
-        <div className="mx-auto">
-          <button
-            type="button"
-            className="btn btn-outline-secondary mx-2"
-            onClick={discardTokenHandler}
-            disabled={props.tokenGtoP == null || props.tokenPtoG == null}
-          >
-            { t('admin:slack_integration.access_token_settings.discard') }
-          </button>
-          <button
-            type="button"
-            className="btn btn-primary mx-2"
-            onClick={generateTokenHandler}
-          >
-            { t('admin:slack_integration.access_token_settings.generate') }
-          </button>
-        </div>
+        <button
+          type="button"
+          className="btn btn-primary mx-auto"
+          onClick={onClickGenerateTokenBtn}
+        >
+          { t('admin:slack_integration.access_token_settings.regenerate') }
+        </button>
       </div>
       <p className="font-weight-bold">2. {t('admin:slack_integration.accordion.register_for_growi_official_bot_proxy_service')}</p>
       <div className="d-flex flex-column align-items-center">
@@ -179,34 +167,27 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers
   );
 }, [AppContainer]);
 
-const TestProcess = () => {
+const TestProcess = ({ apiv3Post, slackAppIntegrationId }) => {
   const { t } = useTranslation();
   const [testChannel, setTestChannel] = useState('');
-  /* eslint-disable no-unused-vars */
-  // TODO: Add connection Logs
-  const [connectionErrorCode, setConnectionErrorCode] = useState(null);
-  const [connectionErrorMessage, setConnectionErrorMessage] = useState(null);
-  const [connectionSuccessMessage, setConnectionSuccessMessage] = useState(null);
+  const [connectionError, setConnectionError] = useState(null);
 
-  // TODO: Show test logs
   let value = '';
-  if (connectionErrorMessage != null) {
-    value = [connectionErrorCode, connectionErrorMessage];
-  }
-  if (connectionSuccessMessage != null) {
-    value = connectionSuccessMessage;
+  if (connectionError != null) {
+    value = [connectionError.code, connectionError.message];
   }
 
-
-  // TODO: Handle test button
-  const submitForm = (e) => {
+  const submitForm = async(e) => {
     e.preventDefault();
-    // eslint-disable-next-line no-console
-    console.log('Form Submitted');
-  };
+    setConnectionError(null);
 
-  const inputTestChannelHandler = (channel) => {
-    setTestChannel(channel);
+    try {
+      await apiv3Post('/slack-integration-settings/with-proxy/relation-test', { slackAppIntegrationId, channel: testChannel });
+    }
+    catch (error) {
+      setConnectionError(error[0]);
+      logger.error(error);
+    }
   };
 
   return (
@@ -223,22 +204,22 @@ const TestProcess = () => {
               type="text"
               value={testChannel}
               placeholder="Slack Channel"
-            // TODO: Handle test button
-              onChange={e => inputTestChannelHandler(e.target.value)}
+              onChange={e => setTestChannel(e.target.value)}
             />
           </div>
           <button
             type="submit"
             className="btn btn-info mx-3 font-weight-bold"
             disabled={testChannel.trim() === ''}
-          >Test
+          >
+            Test
           </button>
         </form>
       </div>
-      {connectionErrorMessage != null
-      && <p className="text-danger text-center my-4">{t('admin:slack_integration.accordion.error_check_logs_below')}</p>}
-      {connectionSuccessMessage != null
-      && <p className="text-info text-center my-4">{t('admin:slack_integration.accordion.send_message_to_slack_work_space')}</p>}
+      {connectionError == null
+        ? <p className="text-info text-center my-4">{t('admin:slack_integration.accordion.send_message_to_slack_work_space')}</p>
+        : <p className="text-danger text-center my-4">{t('admin:slack_integration.accordion.error_check_logs_below')}</p>
+      }
       <form>
         <div className="row my-3 justify-content-center">
           <div className="form-group slack-connection-log col-md-4">
@@ -246,7 +227,6 @@ const TestProcess = () => {
             <textarea
               className="form-control card border-info slack-connection-log-body rounded-lg"
               rows="5"
-            // TODO: Show test logs
               value={value}
               readOnly
             />
@@ -270,8 +250,7 @@ const WithProxyAccordions = (props) => {
       title: 'register_for_growi_official_bot_proxy_service',
       content: <GeneratingTokensAndRegisteringProxyServiceProcess
         growiUrl={props.appContainer.config.crowi.url}
-        discardTokenHandler={props.discardTokenHandler}
-        generateTokenHandler={props.generateTokenHandler}
+        onClickGenerateTokenBtn={props.onClickGenerateTokenBtn}
         tokenPtoG={props.tokenPtoG}
         tokenGtoP={props.tokenGtoP}
       />,
@@ -299,8 +278,7 @@ const WithProxyAccordions = (props) => {
       title: 'register_for_growi_official_bot_proxy_service',
       content: <GeneratingTokensAndRegisteringProxyServiceProcess
         growiUrl={props.appContainer.config.crowi.url}
-        discardTokenHandler={props.discardTokenHandler}
-        generateTokenHandler={props.generateTokenHandler}
+        onClickGenerateTokenBtn={props.onClickGenerateTokenBtn}
         tokenPtoG={props.tokenPtoG}
         tokenGtoP={props.tokenGtoP}
       />,
@@ -311,7 +289,7 @@ const WithProxyAccordions = (props) => {
     },
     '⑤': {
       title: 'test_connection',
-      content: <TestProcess />,
+      content: <TestProcess apiv3Post={props.appContainer.apiv3.post} slackAppIntegrationId={props.slackAppIntegrationId} />,
     },
   };
 
@@ -322,7 +300,6 @@ const WithProxyAccordions = (props) => {
       className="card border-0 rounded-lg shadow overflow-hidden"
     >
       {Object.entries(integrationProcedureMapping).map(([key, value]) => {
-
         return (
           <Accordion
             title={<><span className="mr-2">{key}</span>{t(`admin:slack_integration.accordion.${value.title}`)}</>}
@@ -340,14 +317,15 @@ const WithProxyAccordions = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const OfficialBotSettingsAccordionsWrapper = withUnstatedContainers(WithProxyAccordions, [AppContainer]);
+const WithProxyAccordionsWrapper = withUnstatedContainers(WithProxyAccordions, [AppContainer]);
 WithProxyAccordions.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   botType: PropTypes.string.isRequired,
-  discardTokenHandler: PropTypes.func,
-  generateTokenHandler: PropTypes.func,
+
+  slackAppIntegrationId: PropTypes.string.isRequired,
+  onClickGenerateTokenBtn: PropTypes.func,
   tokenPtoG: PropTypes.string,
   tokenGtoP: PropTypes.string,
 };
 
-export default OfficialBotSettingsAccordionsWrapper;
+export default WithProxyAccordionsWrapper;

+ 62 - 38
src/server/routes/apiv3/slack-integration-settings.js

@@ -5,7 +5,7 @@ const axios = require('axios');
 const urljoin = require('url-join');
 const loggerFactory = require('@alias/logger');
 
-const { getConnectionStatuses, relationTestToSlack } = require('@growi/slack');
+const { getConnectionStatuses, testToSlack, sendSuccessMessage } = require('@growi/slack');
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
@@ -59,16 +59,12 @@ module.exports = (crowi) => {
       body('proxyUri').if(value => value !== '').trim().matches(/^(https?:\/\/)/)
         .isURL({ require_tld: false }),
     ],
-    AccessTokens: [
-      query('tokenGtoP').trim().not().isEmpty()
-        .isString()
-        .isLength({ min: 1 }),
-      query('tokenPtoG').trim().not().isEmpty()
-        .isString()
-        .isLength({ min: 1 }),
-    ],
     RelationTest: [
-      body('slackappintegrationsId').isMongoId(),
+      body('slackAppIntegrationId').isMongoId(),
+      body('channel').trim().isString(),
+    ],
+    deleteIntegration: [
+      query('integrationIdToDelete').isMongoId(),
     ],
     SlackChannel: [
       body('channel').trim().not().isEmpty()
@@ -170,22 +166,29 @@ module.exports = (crowi) => {
       }
     }
     else {
+      try {
+        const slackAppIntegrations = await SlackAppIntegration.find();
+        settings.slackAppIntegrations = slackAppIntegrations;
+      }
+      catch (error) {
+        const msg = 'Error occured in getting connection statuses';
+        logger.error('Error', error);
+        return res.apiv3Err(new ErrorV3(msg, 'get-connection-failed'), 500);
+      }
+
       const proxyServerUri = settings.proxyServerUri;
 
       if (proxyServerUri != null) {
         try {
-          const slackAppIntegrations = await SlackAppIntegration.find();
-          settings.slackAppIntegrations = slackAppIntegrations;
-
-          if (slackAppIntegrations.length > 0) {
-            const tokenGtoPs = slackAppIntegrations.map(slackAppIntegration => slackAppIntegration.tokenGtoP);
+          if (settings.slackAppIntegrations.length > 0) {
+            const tokenGtoPs = settings.slackAppIntegrations.map(slackAppIntegration => slackAppIntegration.tokenGtoP);
             connectionStatuses = (await getConnectionStatusesFromProxy(tokenGtoPs)).connectionStatuses;
           }
         }
         catch (error) {
-          const msg = 'Error occured in getting connection statuses';
+          const msg = 'Incorrect Proxy URL';
           logger.error('Error', error);
-          return res.apiv3Err(new ErrorV3(msg, 'get-connection-failed'), 500);
+          return res.apiv3Err(new ErrorV3(msg, 'test-connection-failed'), 400);
         }
       }
     }
@@ -360,17 +363,17 @@ module.exports = (crowi) => {
   /**
    * @swagger
    *
-   *    /slack-integration-settings/access-tokens:
+   *    /slack-integration-settings/slack-app-integrations:
    *      put:
    *        tags: [SlackIntegration]
-   *        operationId: putAccessTokens
+   *        operationId: putSlackAppIntegrations
    *        summary: /slack-integration
-   *        description: Generate accessTokens
+   *        description: Generate SlackAppIntegrations
    *        responses:
    *          200:
-   *            description: Succeeded to update access tokens for slack
+   *            description: Succeeded to create slack app integration
    */
-  router.put('/access-tokens', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+  router.put('/slack-app-integrations', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
     let checkTokens;
     let tokenGtoP;
     let tokenPtoG;
@@ -406,11 +409,11 @@ module.exports = (crowi) => {
    *          200:
    *            description: Succeeded to delete access tokens for slack
    */
-  router.delete('/slack-app-integration', validator.AccessTokens, apiV3FormValidator, async(req, res) => {
+  router.delete('/slack-app-integration', validator.deleteIntegration, apiV3FormValidator, async(req, res) => {
     const SlackAppIntegration = mongoose.model('SlackAppIntegration');
-    const { tokenGtoP, tokenPtoG } = req.query;
+    const { integrationIdToDelete } = req.query;
     try {
-      const response = await SlackAppIntegration.findOneAndDelete({ tokenGtoP, tokenPtoG });
+      const response = await SlackAppIntegration.findOneAndDelete({ _id: integrationIdToDelete });
       return res.apiv3({ response });
     }
     catch (error) {
@@ -452,7 +455,7 @@ module.exports = (crowi) => {
    *            application/json:
    *              schema:
    *                properties:
-   *                  slackappintegrationsId:
+   *                  slackAppIntegrationId:
    *                    type: string
    *        responses:
    *           200:
@@ -465,23 +468,35 @@ module.exports = (crowi) => {
       return res.apiv3Err(new ErrorV3(msg, 'not-proxy-type'), 400);
     }
 
-    const { slackappintegrationsId } = req.body;
-
+    const { slackAppIntegrationId } = req.body;
+    let slackBotToken;
     try {
-      const slackAppIntegration = await SlackAppIntegration.findOne({ _id: slackappintegrationsId });
+      const slackAppIntegration = await SlackAppIntegration.findOne({ _id: slackAppIntegrationId });
       if (slackAppIntegration == null) {
         const msg = 'Could not find SlackAppIntegration by id';
         return res.apiv3Err(new ErrorV3(msg, 'find-slackAppIntegration-failed'), 400);
       }
-      const response = await postRelationTest(slackAppIntegration.tokenGtoP);
-
-      return res.apiv3({ response });
+      const result = await postRelationTest(slackAppIntegration.tokenGtoP);
+      slackBotToken = result.slackBotToken;
+      if (slackBotToken == null) {
+        const msg = 'Could not find slackBotToken by relation';
+        return res.apiv3Err(new ErrorV3(msg, 'find-slackBotToken-failed'), 400);
+      }
     }
     catch (error) {
-      const msg = 'Error occured in updating Custom bot setting';
       logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(msg, 'update-CustomBotSetting-failed'), 500);
+      return res.apiv3Err(new ErrorV3(`Error occured while testing. Cause: ${error.message}`, 'test-failed', error.stack));
+    }
+
+    const { channel } = req.body;
+    const appSiteURL = crowi.configManager.getConfig('crowi', 'app:siteUrl');
+    try {
+      await sendSuccessMessage(slackBotToken, channel, appSiteURL);
     }
+    catch (error) {
+      return res.apiv3Err(new ErrorV3(`Error occured while sending message. Cause: ${error.message}`, 'send-message-failed', error.stack));
+    }
+
   });
 
   /**
@@ -510,17 +525,26 @@ module.exports = (crowi) => {
       const msg = 'Select Without Proxy Type';
       return res.apiv3Err(new ErrorV3(msg, 'select-not-proxy-type'), 400);
     }
-    // TODO impl req.body at GW-5998
-    // const { channel } = req.body;
+
     const slackBotToken = crowi.configManager.getConfig('crowi', 'slackbot:token');
     try {
-      await relationTestToSlack(slackBotToken);
-      // TODO impl return response after imple 5996, 6002
+      await testToSlack(slackBotToken);
     }
     catch (error) {
       logger.error('Error', error);
       return res.apiv3Err(new ErrorV3(`Error occured while testing. Cause: ${error.message}`, 'test-failed', error.stack));
     }
+
+    const { channel } = req.body;
+    const appSiteURL = crowi.configManager.getConfig('crowi', 'app:siteUrl');
+    try {
+      await sendSuccessMessage(slackBotToken, channel, appSiteURL);
+    }
+    catch (error) {
+      return res.apiv3Err(new ErrorV3(`Error occured while sending message. Cause: ${error.message}`, 'send-message-failed', error.stack));
+    }
+
+    return res.apiv3();
   });
 
   return router;

+ 61 - 11
src/server/routes/apiv3/slack-integration.js

@@ -1,9 +1,10 @@
 const express = require('express');
 const mongoose = require('mongoose');
+const urljoin = require('url-join');
 
 const loggerFactory = require('@alias/logger');
 
-const { verifySlackRequest } = require('@growi/slack');
+const { verifySlackRequest, generateWebClient } = require('@growi/slack');
 
 const logger = loggerFactory('growi:routes:apiv3:slack-integration');
 const router = express.Router();
@@ -24,13 +25,13 @@ module.exports = (crowi) => {
       return res.status(400).send({ message });
     }
 
-    const slackAppIntegration = await SlackAppIntegration.estimatedDocumentCount({ tokenPtoG });
+    const slackAppIntegrationCount = await SlackAppIntegration.estimatedDocumentCount({ tokenPtoG });
 
     logger.debug('verifyAccessTokenFromProxy', {
       tokenPtoG,
     });
 
-    if (slackAppIntegration === 0) {
+    if (slackAppIntegrationCount === 0) {
       return res.status(403).send({
         message: 'The access token that identifies the request source is slackbot-proxy is invalid. Did you setup with `/growi register`?',
       });
@@ -44,6 +45,31 @@ module.exports = (crowi) => {
     return next();
   };
 
+  const generateClientForResponse = (tokenGtoP) => {
+    const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
+
+    if (currentBotType == null) {
+      throw new Error('The config \'SLACK_BOT_TYPE\'(ns: \'crowi\', key: \'slackbot:currentBotType\') must be set.');
+    }
+
+    let token;
+
+    // connect directly
+    if (tokenGtoP == null) {
+      token = crowi.configManager.getConfig('crowi', 'slackbot:token');
+      return generateWebClient(token);
+    }
+
+    // connect to proxy
+    const proxyServerUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyServerUri');
+    const serverUri = urljoin(proxyServerUri, '/g2s');
+    const headers = {
+      'x-growi-gtop-tokens': tokenGtoP,
+    };
+
+    return generateWebClient(token, serverUri, headers);
+  };
+
   async function handleCommands(req, res) {
     const { body } = req;
 
@@ -59,19 +85,31 @@ module.exports = (crowi) => {
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
     res.send();
 
+    const tokenPtoG = req.headers['x-growi-ptog-tokens'];
+
+    // generate client
+    let client;
+    if (tokenPtoG == null) {
+      client = generateClientForResponse();
+    }
+    else {
+      const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
+      client = generateClientForResponse(slackAppIntegration.tokenGtoP);
+    }
+
     const args = body.text.split(' ');
     const command = args[0];
 
     try {
       switch (command) {
         case 'search':
-          await crowi.slackBotService.showEphemeralSearchResults(body, args);
+          await crowi.slackBotService.showEphemeralSearchResults(client, body, args);
           break;
         case 'create':
-          await crowi.slackBotService.createModal(body);
+          await crowi.slackBotService.createModal(client, body);
           break;
         default:
-          await crowi.slackBotService.notCommand(body);
+          await crowi.slackBotService.notCommand(client, body);
           break;
       }
     }
@@ -98,12 +136,12 @@ module.exports = (crowi) => {
   });
 
 
-  const handleBlockActions = async(payload) => {
+  const handleBlockActions = async(client, payload) => {
     const { action_id: actionId } = payload.actions[0];
 
     switch (actionId) {
       case 'shareSearchResults': {
-        await crowi.slackBotService.shareSearchResults(payload);
+        await crowi.slackBotService.shareSearchResults(client, payload);
         break;
       }
       case 'showNextResults': {
@@ -111,7 +149,7 @@ module.exports = (crowi) => {
 
         const { body, args, offset } = parsedValue;
         const newOffset = offset + 10;
-        await crowi.slackBotService.showEphemeralSearchResults(body, args, newOffset);
+        await crowi.slackBotService.showEphemeralSearchResults(client, body, args, newOffset);
         break;
       }
       default:
@@ -137,16 +175,28 @@ module.exports = (crowi) => {
     // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
     res.send();
 
+
+    const tokenPtoG = req.headers['x-growi-ptog-tokens'];
+    // generate client
+    let client;
+    if (tokenPtoG == null) {
+      client = generateClientForResponse();
+    }
+    else {
+      const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
+      client = generateClientForResponse(slackAppIntegration.tokenGtoP);
+    }
+
     const payload = JSON.parse(req.body.payload);
     const { type } = payload;
 
     try {
       switch (type) {
         case 'block_actions':
-          await handleBlockActions(payload);
+          await handleBlockActions(client, payload);
           break;
         case 'view_submission':
-          await handleViewSubmission(payload);
+          await handleViewSubmission(client, payload);
           break;
         default:
           break;

+ 16 - 50
src/server/service/slackbot.js

@@ -1,8 +1,6 @@
 const logger = require('@alias/logger')('growi:service:SlackBotService');
 const mongoose = require('mongoose');
 
-const { generateWebClient } = require('@growi/slack');
-
 const PAGINGLIMIT = 10;
 
 const S2sMessage = require('../models/vo/s2s-message');
@@ -25,29 +23,6 @@ class SlackBotService extends S2sMessageHandlable {
     this.lastLoadedAt = new Date();
   }
 
-  get client() {
-    const currentBotType = this.crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
-
-    if (currentBotType == null) {
-      throw new Error('The config \'SLACK_BOT_TYPE\'(ns: \'crowi\', key: \'slackbot:currentBotType\') must be set.');
-    }
-
-    let serverUri;
-    let token;
-
-    // connect to proxy
-    if (currentBotType !== 'customBotWithoutProxy') {
-      // TODO: https://youtrack.weseek.co.jp/issue/GW-5896
-      serverUri = 'http://localhost:8080/slack-api-proxy/';
-    }
-    // connect directly
-    else {
-      token = this.crowi.configManager.getConfig('crowi', 'slackbot:token');
-    }
-
-    return generateWebClient(token, serverUri);
-  }
-
   /**
    * @inheritdoc
    */
@@ -87,13 +62,9 @@ class SlackBotService extends S2sMessageHandlable {
     }
   }
 
-  async sendAuthTest() {
-    await this.client.api.test();
-  }
-
-  notCommand(body) {
+  async notCommand(client, body) {
     logger.error('Invalid first argument');
-    this.client.chat.postEphemeral({
+    client.chat.postEphemeral({
       channel: body.channel_id,
       user: body.user_id,
       blocks: [
@@ -109,10 +80,10 @@ class SlackBotService extends S2sMessageHandlable {
     return keywords;
   }
 
-  async getSearchResultPaths(body, args, offset = 0) {
+  async getSearchResultPaths(client, body, args, offset = 0) {
     const firstKeyword = args[1];
     if (firstKeyword == null) {
-      this.client.chat.postEphemeral({
+      client.chat.postEphemeral({
         channel: body.channel_id,
         user: body.user_id,
         blocks: [
@@ -132,7 +103,7 @@ class SlackBotService extends S2sMessageHandlable {
     // no search results
     if (results.data.length === 0) {
       logger.info(`No page found with "${keywords}"`);
-      this.client.chat.postEphemeral({
+      client.chat.postEphemeral({
         channel: body.channel_id,
         user: body.user_id,
         blocks: [
@@ -166,22 +137,17 @@ class SlackBotService extends S2sMessageHandlable {
     };
   }
 
-  async getSlackChannelName() {
-    const slackTeamInfo = await this.client.team.info();
-    return slackTeamInfo.team.name;
-  }
-
-  shareSearchResults(payload) {
-    this.client.chat.postMessage({
+  async shareSearchResults(client, payload) {
+    client.chat.postMessage({
       channel: payload.channel.id,
       text: payload.actions[0].value,
     });
   }
 
-  async showEphemeralSearchResults(body, args, offsetNum) {
+  async showEphemeralSearchResults(client, body, args, offsetNum) {
     const {
       resultPaths, offset, resultsTotal,
-    } = await this.getSearchResultPaths(body, args, offsetNum);
+    } = await this.getSearchResultPaths(client, body, args, offsetNum);
 
     const keywords = this.getKeywords(args);
 
@@ -246,7 +212,7 @@ class SlackBotService extends S2sMessageHandlable {
           },
         );
       }
-      await this.client.chat.postEphemeral({
+      await client.chat.postEphemeral({
         channel: body.channel_id,
         user: body.user_id,
         blocks: [
@@ -258,7 +224,7 @@ class SlackBotService extends S2sMessageHandlable {
     }
     catch {
       logger.error('Failed to get search results.');
-      await this.client.chat.postEphemeral({
+      await client.chat.postEphemeral({
         channel: body.channel_id,
         user: body.user_id,
         blocks: [
@@ -269,9 +235,9 @@ class SlackBotService extends S2sMessageHandlable {
     }
   }
 
-  async createModal(body) {
+  async createModal(client, body) {
     try {
-      await this.client.views.open({
+      await client.views.open({
         trigger_id: body.trigger_id,
 
         view: {
@@ -299,7 +265,7 @@ class SlackBotService extends S2sMessageHandlable {
     }
     catch (err) {
       logger.error('Failed to create a page.');
-      await this.client.chat.postEphemeral({
+      await client.chat.postEphemeral({
         channel: body.channel_id,
         user: body.user_id,
         blocks: [
@@ -311,7 +277,7 @@ class SlackBotService extends S2sMessageHandlable {
   }
 
   // Submit action in create Modal
-  async createPageInGrowi(payload) {
+  async createPageInGrowi(client, payload) {
     const Page = this.crowi.model('Page');
     const pathUtils = require('growi-commons').pathUtils;
 
@@ -328,7 +294,7 @@ class SlackBotService extends S2sMessageHandlable {
       await Page.create(path, contentsBody, dummyObjectIdOfUser, {});
     }
     catch (err) {
-      this.client.chat.postMessage({
+      client.chat.postMessage({
         channel: payload.user.id,
         blocks: [
           this.generateMarkdownSectionBlock(`Cannot create new page to existed path\n *Contents* :memo:\n ${contentsBody}`)],