Procházet zdrojové kódy

Merge branch '5827-With-Proxy-Accordion-3-4' into imprv/gw5825-add-accordion-contents-2-3

kaori před 5 roky
rodič
revize
fe3f4b0e00
25 změnil soubory, kde provedl 841 přidání a 638 odebrání
  1. 1 0
      packages/slack/package.json
  2. 3 1
      packages/slack/src/index.ts
  3. 9 0
      packages/slack/src/interfaces/request-from-slack.ts
  4. 13 15
      packages/slack/src/middlewares/verify-slack-request.ts
  5. 40 0
      packages/slack/src/utils/post-ephemeral-errors.ts
  6. 1 0
      packages/slackbot-proxy/docker-compose.dev.yml
  7. 48 85
      packages/slackbot-proxy/src/controllers/slack.ts
  8. 14 0
      packages/slackbot-proxy/src/middlewares/add-signing-secret-to-req.ts
  9. 69 3
      packages/slackbot-proxy/src/services/RegisterService.ts
  10. 5 0
      resource/locales/en_US/admin/admin.json
  11. 7 0
      resource/locales/ja_JP/admin/admin.json
  12. 5 0
      resource/locales/zh_CN/admin/admin.json
  13. 10 3
      src/client/js/components/Admin/Common/AdminNavigation.jsx
  14. 49 43
      src/client/js/components/Admin/SlackIntegration/CustomBotWithProxyIntegrationCard.jsx
  15. 3 0
      src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  16. 14 9
      src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettingsAccordion.jsx
  17. 23 23
      src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxyIntegrationCard.jsx
  18. 47 4
      src/client/js/components/Admin/SlackIntegration/OfficialbotSettingsAccordion.jsx
  19. 3 3
      src/server/routes/admin.js
  20. 1 1
      src/server/routes/apiv3/index.js
  21. 0 135
      src/server/routes/apiv3/slack-bot.js
  22. 354 0
      src/server/routes/apiv3/slack-integration-legacy.js
  23. 121 312
      src/server/routes/apiv3/slack-integration.js
  24. 1 1
      src/server/routes/index.js
  25. 0 0
      src/server/views/admin/slack-integration-legacy.html

+ 1 - 0
packages/slack/package.json

@@ -22,6 +22,7 @@
   },
   "devDependencies": {
     "@slack/bolt": "^3.3.0",
+    "@types/express": "^4.17.11",
     "@types/jest": "^26.0.22",
     "@typescript-eslint/eslint-plugin": "^4.18.0",
     "@typescript-eslint/parser": "^4.18.0",

+ 3 - 1
packages/slack/src/index.ts

@@ -8,8 +8,10 @@ export const supportedGrowiCommands: string[] = [
 ];
 
 export * from './interfaces/growi-command';
+export * from './interfaces/request-from-slack';
 export * from './models/errors';
-export * from './middlewares/verification-slack-request';
+export * from './middlewares/verify-slack-request';
 export * from './utils/block-creater';
+export * from './utils/post-ephemeral-errors';
 export * from './utils/slash-command-parser';
 export * from './utils/webclient-factory';

+ 9 - 0
packages/slack/src/interfaces/request-from-slack.ts

@@ -0,0 +1,9 @@
+import { Request } from 'express';
+
+export type RequestFromSlack = Request & {
+  // appended by slack
+  headers:{'x-slack-signature'?:string, 'x-slack-request-timestamp':number},
+
+  // appended by GROWI or slackbot-proxy
+  slackSigningSecret?:string,
+};

+ 13 - 15
packages/slack/src/middlewares/verification-slack-request.ts → packages/slack/src/middlewares/verify-slack-request.ts

@@ -1,19 +1,18 @@
 import { createHmac, timingSafeEqual } from 'crypto';
 import { stringify } from 'qs';
-import { Request, Response, NextFunction } from 'express';
+import { Response, NextFunction } from 'express';
+
+import { RequestFromSlack } from '../interfaces/request-from-slack';
+
 /**
-   * Verify if the request came from slack
-   * See: https://api.slack.com/authentication/verifying-requests-from-slack
-   */
-
-type signingSecretType = {
-  signingSecret?:string; headers:{'x-slack-signature'?:string, 'x-slack-request-timestamp':number}
-}
-
-// eslint-disable-next-line max-len
-export const verificationSlackRequest = (req : Request & signingSecretType, res:Response, next:NextFunction):Record<string, any>| void => {
-  if (req.signingSecret == null) {
-    return res.send('No signing secret.');
+ * Verify if the request came from slack
+ * See: https://api.slack.com/authentication/verifying-requests-from-slack
+ */
+export const verifySlackRequest = (req: RequestFromSlack, res: Response, next: NextFunction): Record<string, any> | void => {
+  const signingSecret = req.slackSigningSecret;
+
+  if (signingSecret == null) {
+    return res.status(400).send({ message: 'No signing secret.' });
   }
 
   // take out slackSignature and timestamp from header
@@ -32,7 +31,7 @@ export const verificationSlackRequest = (req : Request & signingSecretType, res:
 
   // generate growi signature
   const sigBaseString = `v0:${timestamp}:${stringify(req.body, { format: 'RFC1738' })}`;
-  const hasher = createHmac('sha256', req.signingSecret);
+  const hasher = createHmac('sha256', signingSecret);
   hasher.update(sigBaseString, 'utf8');
   const hashedSigningSecret = hasher.digest('hex');
   const growiSignature = `v0=${hashedSigningSecret}`;
@@ -40,7 +39,6 @@ export const verificationSlackRequest = (req : Request & signingSecretType, res:
   // compare growiSignature and slackSignature
   if (timingSafeEqual(Buffer.from(growiSignature, 'utf8'), Buffer.from(slackSignature, 'utf8'))) {
     return next();
-
   }
 
   return res.send('Verification failed.');

+ 40 - 0
packages/slack/src/utils/post-ephemeral-errors.ts

@@ -0,0 +1,40 @@
+import { WebAPICallResult } from '@slack/web-api';
+
+import { generateMarkdownSectionBlock } from './block-creater';
+import { generateWebClient } from './webclient-factory';
+
+export const postEphemeralErrors = async(
+  rejectedResults: PromiseRejectedResult[],
+  channelId: string,
+  userId: string,
+  botToken: string,
+): Promise<WebAPICallResult|void> => {
+
+  if (rejectedResults.length > 0) {
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const client = generateWebClient(botToken);
+
+    return client.chat.postEphemeral({
+      text: 'Error occured.',
+      channel: channelId,
+      user: userId,
+      blocks: [
+        generateMarkdownSectionBlock('*Error occured:*'),
+        ...rejectedResults.map((rejectedResult) => {
+          const reason = rejectedResult.reason.toString();
+          const resData = rejectedResult.reason.response?.data;
+          const resDataMessage = resData?.message || resData?.toString();
+
+          let errorMessage = reason;
+          if (resDataMessage != null) {
+            errorMessage += ` (${resDataMessage})`;
+          }
+
+          return generateMarkdownSectionBlock(errorMessage);
+        }),
+      ],
+    });
+  }
+
+  return;
+};

+ 1 - 0
packages/slackbot-proxy/docker-compose.dev.yml

@@ -11,6 +11,7 @@ services:
       - MYSQL_USER=growi-slackbot-proxy
       - MYSQL_PASSWORD=YrkUi7rCW46Z2N6EXSFUBwaQTUR8biCU
       - MYSQL_DATABASE=growi-slackbot-proxy
+    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
     volumes:
       - /data/db
 

+ 48 - 85
packages/slackbot-proxy/src/controllers/slack.ts

@@ -4,22 +4,27 @@ import {
 
 import axios from 'axios';
 
-import { generateMarkdownSectionBlock, generateWebClient, parseSlashCommand } from '@growi/slack';
-import { Installation } from '~/entities/installation';
+import { WebAPICallResult } from '@slack/web-api';
 
+import {
+  generateMarkdownSectionBlock, parseSlashCommand, postEphemeralErrors, verifySlackRequest,
+} from '@growi/slack';
+
+import { Relation } from '~/entities/relation';
+import { AuthedReq } from '~/interfaces/authorized-req';
 import { InstallationRepository } from '~/repositories/installation';
 import { RelationRepository } from '~/repositories/relation';
 import { OrderRepository } from '~/repositories/order';
+import { AddSigningSecretToReq } from '~/middlewares/add-signing-secret-to-req';
+import { AuthorizeCommandMiddleware, AuthorizeInteractionMiddleware } from '~/middlewares/authorizer';
 import { InstallerService } from '~/services/InstallerService';
 import { RegisterService } from '~/services/RegisterService';
-
 import loggerFactory from '~/utils/logger';
-import { AuthorizeCommandMiddleware, AuthorizeInteractionMiddleware } from '~/middlewares/authorizer';
-import { AuthedReq } from '~/interfaces/authorized-req';
-import { Relation } from '~/entities/relation';
+
 
 const logger = loggerFactory('slackbot-proxy:controllers:slack');
 
+
 @Controller('/slack')
 export class SlackCtrl {
 
@@ -38,25 +43,6 @@ export class SlackCtrl {
   @Inject()
   registerService: RegisterService;
 
-  @Get('/testsave')
-  testsave(): void {
-    const installation = new Installation();
-    installation.data = {
-      team: undefined,
-      enterprise: undefined,
-      user: {
-        id: '',
-        token: undefined,
-        scopes: undefined,
-      },
-    };
-
-    // const installationRepository = getRepository(Installation);
-
-    this.installationRepository.save(installation);
-  }
-
-
   @Get('/install')
   async install(): Promise<string> {
     const url = await this.installerService.installer.generateInstallUrl({
@@ -78,24 +64,23 @@ export class SlackCtrl {
   }
 
   @Post('/commands')
-  @UseBefore(AuthorizeCommandMiddleware)
-  async handleCommand(@Req() req: AuthedReq, @Res() res: Res): Promise<void|string> {
+  @UseBefore(AddSigningSecretToReq, verifySlackRequest, AuthorizeCommandMiddleware)
+  async handleCommand(@Req() req: AuthedReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
     const { body, authorizeResult } = req;
 
     if (body.text == null) {
       return 'No text.';
     }
 
-    // Send response immediately to avoid opelation_timeout error
-    // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
-    res.send();
-
     const growiCommand = parseSlashCommand(body);
 
     // register
     if (growiCommand.growiCommandType === 'register') {
-      await this.registerService.process(growiCommand, authorizeResult, body as {[key:string]:string});
-      return;
+      // Send response immediately to avoid opelation_timeout error
+      // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
+      res.send();
+
+      return this.registerService.process(growiCommand, authorizeResult, body as {[key:string]:string});
     }
 
     /*
@@ -106,9 +91,22 @@ export class SlackCtrl {
     const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
     const relations = await this.relationRepository.find({ installation: installation?.id });
 
+    if (relations.length === 0) {
+      return res.json({
+        blocks: [
+          generateMarkdownSectionBlock('*No relation found.*'),
+          generateMarkdownSectionBlock('Run `/growi register` first.'),
+        ],
+      });
+    }
+
+    // Send response immediately to avoid opelation_timeout error
+    // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
+    res.send();
+
     const promises = relations.map((relation: Relation) => {
       // generate API URL
-      const url = new URL('/_api/v3/slack-bot/commands', relation.growiUri);
+      const url = new URL('/_api/v3/slack-integration/proxied/commands', relation.growiUri);
       return axios.post(url.toString(), {
         ...body,
         tokenPtoG: relation.tokenPtoG,
@@ -119,27 +117,14 @@ export class SlackCtrl {
     // pickup PromiseRejectedResult only
     const results = await Promise.allSettled(promises);
     const rejectedResults: PromiseRejectedResult[] = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
+    const botToken = installation?.data.bot?.token;
 
-    if (rejectedResults.length > 0) {
-      const botToken = installation?.data.bot?.token;
-
+    try {
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      const client = generateWebClient(botToken!);
-
-      try {
-        client.chat.postEphemeral({
-          text: 'Error occured.',
-          channel: body.channel_id,
-          user: body.user_id,
-          blocks: [
-            generateMarkdownSectionBlock('*Error occured:*'),
-            ...rejectedResults.map(result => generateMarkdownSectionBlock(result.reason.toString())),
-          ],
-        });
-      }
-      catch (err) {
-        logger.error(err);
-      }
+      return postEphemeralErrors(rejectedResults, body.channel_id, body.user_id, botToken!);
+    }
+    catch (err) {
+      logger.error(err);
     }
   }
 
@@ -164,43 +149,21 @@ export class SlackCtrl {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
 
-    const handleViewSubmission = async(inputValues) => {
-
-      const inputGrowiUrl = inputValues.growiDomain.contents_input.value;
-      const inputGrowiAccessToken = inputValues.growiAccessToken.contents_input.value;
-      const inputProxyAccessToken = inputValues.proxyToken.contents_input.value;
-
-      const order = await this.orderRepository.findOne({ installation: installation?.id, growiUrl: inputGrowiUrl });
-      if (order != null) {
-        this.orderRepository.update(
-          { installation: installation?.id, growiUrl: inputGrowiUrl },
-          { growiAccessToken: inputGrowiAccessToken, proxyAccessToken: inputProxyAccessToken },
-        );
-      }
-      else {
-        this.orderRepository.save({
-          installation: installation?.id, growiUrl: inputGrowiUrl, growiAccessToken: inputGrowiAccessToken, proxyAccessToken: inputProxyAccessToken,
-        });
-      }
-    };
-
     const payload = JSON.parse(body.payload);
     const { type } = payload;
-    const inputValues = payload.view.state.values;
 
-    try {
-      switch (type) {
-        case 'view_submission':
-          await handleViewSubmission(inputValues);
-          break;
-        default:
-          break;
-      }
-    }
-    catch (error) {
-      logger.error(error);
+    // register
+    if (type === 'view_submission' && payload.response_urls[0].action_id === 'submit_growi_url_and_access_tokens') {
+      await this.registerService.upsertOrderRecord(this.orderRepository, installation, payload);
+      await this.registerService.showProxyUrl(authorizeResult, payload);
+      return;
     }
 
+    /*
+     * forward to GROWI server
+     */
+    // TODO: forward to GROWI server by GW-5866
+
   }
 
   @Post('/events')

+ 14 - 0
packages/slackbot-proxy/src/middlewares/add-signing-secret-to-req.ts

@@ -0,0 +1,14 @@
+import { RequestFromSlack } from '@growi/slack';
+import {
+  IMiddleware, Middleware, Next, Req, Res,
+} from '@tsed/common';
+
+@Middleware()
+export class AddSigningSecretToReq implements IMiddleware {
+
+  use(@Req() req: Req & RequestFromSlack, @Res() res: Res, @Next() next: Next): void {
+    req.slackSigningSecret = process.env.SLACK_SIGNING_SECRET;
+    next();
+  }
+
+}

+ 69 - 3
packages/slackbot-proxy/src/services/RegisterService.ts

@@ -1,9 +1,10 @@
 import { Service } from '@tsed/di';
 import { WebClient, LogLevel } from '@slack/web-api';
-import { generateInputSectionBlock, GrowiCommand } from '@growi/slack';
+import { generateInputSectionBlock, GrowiCommand, generateMarkdownSectionBlock } from '@growi/slack';
 import { AuthorizeResult } from '@slack/oauth';
-
 import { GrowiCommandProcessor } from '~/interfaces/growi-command-processor';
+import { OrderRepository } from '~/repositories/order';
+import { Installation } from '~/entities/installation';
 
 
 const isProduction = process.env.NODE_ENV === 'production';
@@ -12,7 +13,6 @@ const isProduction = process.env.NODE_ENV === 'production';
 export class RegisterService implements GrowiCommandProcessor {
 
   async process(growiCommand: GrowiCommand, authorizeResult: AuthorizeResult, body: {[key:string]:string}): Promise<void> {
-
     const { botToken } = authorizeResult;
 
     // tmp use process.env
@@ -37,9 +37,75 @@ export class RegisterService implements GrowiCommandProcessor {
           generateInputSectionBlock('growiDomain', 'GROWI domain', 'contents_input', false, 'https://example.com'),
           generateInputSectionBlock('growiAccessToken', 'GROWI ACCESS_TOKEN', 'contents_input', false, 'jBMZvpk.....'),
           generateInputSectionBlock('proxyToken', 'PROXY ACCESS_TOKEN', 'contents_input', false, 'jBMZvpk.....'),
+          {
+            block_id: 'channel_to_post_proxy_url',
+            type: 'input',
+            label: {
+              type: 'plain_text',
+              text: 'Select a channel to post the proxy URL on',
+            },
+            element: {
+              action_id: 'submit_growi_url_and_access_tokens',
+              type: 'conversations_select',
+              response_url_enabled: true,
+              default_to_current_conversation: true,
+            },
+          },
         ],
       },
     });
   }
 
+  async upsertOrderRecord(
+      // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+      orderRepository: OrderRepository, installation: Installation | undefined, payload: any,
+  ): Promise<void> {
+    const inputValues = payload.view.state.values;
+    const inputGrowiUrl = inputValues.growiDomain.contents_input.value;
+    const inputGrowiAccessToken = inputValues.growiAccessToken.contents_input.value;
+    const inputProxyAccessToken = inputValues.proxyToken.contents_input.value;
+
+    const order = await orderRepository.findOne({ installation: installation?.id, growiUrl: inputGrowiUrl });
+    if (order != null) {
+      orderRepository.update(
+        { installation: installation?.id, growiUrl: inputGrowiUrl },
+        { growiAccessToken: inputGrowiAccessToken, proxyAccessToken: inputProxyAccessToken },
+      );
+    }
+    else {
+      orderRepository.save({
+        installation: installation?.id, growiUrl: inputGrowiUrl, growiAccessToken: inputGrowiAccessToken, proxyAccessToken: inputProxyAccessToken,
+      });
+    }
+  }
+
+  async showProxyUrl(
+      // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+      authorizeResult:AuthorizeResult, payload: any,
+  ): Promise<void> {
+
+    // TODO: implement for when proxy URL is undefined by GW-5834
+    let proxyURL;
+    if (process.env.PROXY_URL != null) {
+      proxyURL = process.env.PROXY_URL;
+    }
+
+    const { botToken } = authorizeResult;
+
+    const client = new WebClient(botToken, { logLevel: isProduction ? LogLevel.DEBUG : LogLevel.INFO });
+
+    await client.chat.postEphemeral({
+      channel: payload.response_urls[0].channel_id,
+      user: payload.user.id,
+      // Recommended including 'text' to provide a fallback when using blocks
+      // refer to https://api.slack.com/methods/chat.postEphemeral#text_usage
+      text: 'Proxy URL',
+      blocks: [
+        generateMarkdownSectionBlock('Please enter and update the following Proxy URL to slack bot setting form in your GROWI'),
+        generateMarkdownSectionBlock(`Proxy URL: ${proxyURL}`),
+      ],
+    });
+    return;
+  }
+
 }

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

@@ -296,6 +296,11 @@
       "how_to_install": "How to install",
       "install_bot_to_slack": "Install Bot To Slack",
       "install_now": "Install now",
+      "generate_access_token": "Generate Access Token",
+      "register_for_growi_official_bot_proxy_service": "Register for GROWI Official Bot Proxy Service",
+      "enter_growi_register_on_slack": "Enter `/growi register` on slack",
+      "enter_access_token_for_growi_and_proxy": "Enter Access Token for GROWI and Access Token for Proxy",
+      "set_proxy_url_on_growi": "Set Proxy URL on GROWI",
       "select_install_your_app": "Select \"Install your app\".",
       "select_install_to_workspace": "Select \"Install to Workspace\".",
       "register_official_bot_proxy_service": "Issue Access Token / Register GROWI Official Bot Proxy Service",

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

@@ -293,6 +293,13 @@
       "how_to_install": "インストール方法はこちら",
       "install_bot_to_slack": "Bot を Slack にインストールする",
       "install_now": "今すぐインストール",
+      "generate_access_token": "Access Tokenの発行",
+      "register_for_growi_official_bot_proxy_service": "GROWI Official Bot Proxy サービスへの登録",
+      "enter_growi_register_on_slack": "Slack上で`/growi register`と打つ",
+      "paste_growi_url": "<b>GROWI URL</b>には`http://localhost:3000`を貼り付ける",
+      "enter_access_token_for_growi_and_proxy": "上記で発行した<b>Access Token for GROWI</b> と Access Token for Proxy</b>を入れる",
+      "set_proxy_url_on_growi": "ProxyのURLをGROWIに登録する",
+      "enter_proxy_url_and_update": "Slack上に通知された<b>Proxy URL</b>を入力し、更新してください。",
       "select_install_your_app": "Install your app をクリックします。",
       "select_install_to_workspace": "Install to Workspace をクリックします。",
       "register_official_bot_proxy_service": "アクセストークンの発行 / GROWI Official Bot Proxy サービスへの登録",

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

@@ -303,6 +303,11 @@
       "how_to_install": "点击这里查看安装说明",
       "install_bot_to_slack": "将 Bot 安装到 Slack",
       "install_now": "现在安装",
+      "generate_access_token": "Generate Access Token",
+      "register_for_growi_official_bot_proxy_service": "GROWI Official Bot Proxy サービスへの登録",
+      "enter_growi_register_on_slack": "Slack上で`/growi register`と打つ",
+      "enter_access_token_for_growi_and_proxy": "上記で発行した<b>Access Token for GROWI</b> と Access Token for Proxy</b>を入れる",
+      "set_proxy_url_on_growi": "ProxyのURLをGROWIに登録する",
       "select_install_your_app": "选择 \"Install your app\"。",
       "select_install_to_workspace": "选择 \"Install to Workspace\"。",
       "register_official_bot_proxy_service": "发行访问令牌 / 注册 GROWI 官方 Bot 代理服务",

+ 10 - 3
src/client/js/components/Admin/Common/AdminNavigation.jsx

@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 
+import { pathUtils } from 'growi-commons';
 
 const AdminNavigation = (props) => {
   const { t } = props;
@@ -21,7 +22,7 @@ const AdminNavigation = (props) => {
       case 'importer':                 return <><i className="icon-fw icon-cloud-upload"></i>    { t('Import Data') }</>;
       case 'export':                   return <><i className="icon-fw icon-cloud-download"></i>  { t('Export Archive Data') }</>;
       case 'notification':             return <><i className="icon-fw icon-bell"></i>            { t('External_Notification')}</>;
-      case 'legacy-slack-integration': return <><i className="fa fa-slack mr-2"></i>             { t('Legacy_Slack_Integration')}</>;
+      case 'slack-integration-legacy': return <><i className="fa fa-slack mr-2"></i>             { t('Legacy_Slack_Integration')}</>;
       case 'slack-integration':        return <><i className="fa fa-slack mr-2"></i>             { t('slack_integration') }</>;
       case 'users':                    return <><i className="icon-fw icon-user"></i>            { t('User_Management') }</>;
       case 'user-groups':              return <><i className="icon-fw icon-people"></i>          { t('UserGroup Management') }</>;
@@ -49,7 +50,13 @@ const AdminNavigation = (props) => {
   };
 
   const isActiveMenu = (path) => {
-    return (pathname.startsWith(urljoin('/admin', path)));
+    const basisPath = pathUtils.normalizePath(urljoin('/admin', path));
+    const basisParentPath = pathUtils.addTrailingSlash(basisPath);
+
+    return (
+      pathname === basisPath
+      || pathname.startsWith(basisParentPath)
+    );
   };
 
   const getListGroupItemOrDropdownItemList = (isListGroupItems) => {
@@ -64,7 +71,7 @@ const AdminNavigation = (props) => {
         <MenuLink menu="export"       isListGroupItems isActive={isActiveMenu('/export')} />
         <MenuLink menu="notification" isListGroupItems isActive={isActiveMenu('/notification') || isActiveMenu('/global-notification')} />
         <MenuLink menu="slack-integration" isListGroupItems isActive={isActiveMenu('/slack-integration')} />
-        <MenuLink menu="legacy-slack-integration" isListGroupItems isActive={isActiveMenu('/legacy-slack-integration')} />
+        <MenuLink menu="slack-integration-legacy" isListGroupItems isActive={isActiveMenu('/slack-integration-legacy')} />
         <MenuLink menu="users"        isListGroupItems isActive={isActiveMenu('/users')} />
         <MenuLink menu="user-groups"  isListGroupItems isActive={isActiveMenu('/user-groups')} />
         <MenuLink menu="search"       isListGroupItems isActive={isActiveMenu('/search')} />

+ 49 - 43
src/client/js/components/Admin/SlackIntegration/CustomBotWithProxyIntegrationCard.jsx

@@ -6,63 +6,69 @@ const CustomBotWithProxyIntegrationCard = (props) => {
   const { t } = useTranslation();
 
   return (
-    <>
+    <div className="d-flex justify-content-center my-5 bot-integration">
 
-      <div className="d-flex justify-content-center my-5 bot-integration">
-
-        <div className="card rounded shadow border-0 w-50 admin-bot-card">
-          <h5 className="card-title font-weight-bold mt-3 ml-4">Slack</h5>
-          <div className="card-body p-4"></div>
-        </div>
-
-        <div className="text-center w-25">
-          {props.isSlackScopeSet && (
-            <p className="text-success small">
-              <i className="fa fa-check mr-1" />
-              {t('admin:slack_integration.integration_sentence.integration_successful')}
-            </p>
-          )}
-          {!props.isSlackScopeSet && (
-            <small
-              className="text-secondary"
-              // eslint-disable-next-line react/no-danger
-              dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
-            />
-          )}
-          <div className="pt-2">
-            <div className="position-relative mt-5">
-              <div className="circle position-absolute bg-primary border-light">
-                <p className="circle-inner text-light font-weight-bold">Proxy Server</p>
+      <div className="card rounded shadow border-0 w-50 admin-bot-card">
+        <h5 className="card-title font-weight-bold mt-3 ml-4">Slack</h5>
+        <div className="card-body px-5">
+          {props.slackWSNameInWithProxy != null && (
+            <div className="card slack-work-space-name-card">
+              <div className="m-2 text-center">
+                <h5 className="font-weight-bold">{props.slackWSNameInWithProxy}</h5>
+                <img width={20} height={20} src="/images/slack-integration/growi-bot-kun-icon.png" />
               </div>
-              {props.isSlackScopeSet && (
-                <hr className="align-self-center border-success admin-border-success"></hr>
-              )}
-              {!props.isSlackScopeSet && (
-                <hr className="align-self-center border-danger admin-border-danger"></hr>
-              )}
             </div>
-          </div>
+          )}
         </div>
+      </div>
 
-        <div className="card rounded-lg shadow border-0 w-50 admin-bot-card">
-          <div className="row">
-            <h5 className="card-title font-weight-bold mt-3 ml-4 col">GROWI App</h5>
-            <div className="pull-right mt-3 mr-3">
-              <a className="icon-fw fa fa-repeat fa-2x"></a>
+      <div className="text-center w-25">
+        {props.isSlackScopeSet && (
+          <p className="text-success small">
+            <i className="fa fa-check mr-1" />
+            {t('admin:slack_integration.integration_sentence.integration_successful')}
+          </p>
+        )}
+        {!props.isSlackScopeSet && (
+          <small
+            className="text-secondary"
+            // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
+          />
+        )}
+        <div className="pt-2">
+          <div className="position-relative mt-5">
+            <div className="circle position-absolute bg-primary border-light">
+              <p className="circle-inner text-light font-weight-bold">Proxy Server</p>
             </div>
-          </div>
-          <div className="card-body p-4 mb-5 text-center">
-            <a className="btn btn-primary">WESEEK Inner Wiki</a>
+            {props.isSlackScopeSet && (
+              <hr className="align-self-center border-success admin-border-success"></hr>
+            )}
+            {!props.isSlackScopeSet && (
+              <hr className="align-self-center border-danger admin-border-danger"></hr>
+            )}
           </div>
         </div>
-
       </div>
 
-    </>
+      <div className="card rounded-lg shadow border-0 w-50 admin-bot-card">
+        <div className="row">
+          <h5 className="card-title font-weight-bold mt-3 ml-4 col">GROWI App</h5>
+          <div className="pull-right mt-3 mr-3">
+            <a className="icon-fw fa fa-repeat fa-2x"></a>
+          </div>
+        </div>
+        <div className="card-body p-4 mb-5 text-center">
+          <a className="btn btn-primary">{props.siteName}</a>
+        </div>
+      </div>
+    </div>
   );
 };
 
 CustomBotWithProxyIntegrationCard.propTypes = {
+  siteName: PropTypes.string.isRequired,
+  slackWSNameInWithProxy: PropTypes.string,
   isSlackScopeSet: PropTypes.bool.isRequired,
 };
 

+ 3 - 0
src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -17,7 +17,10 @@ const CustomBotWithProxySettings = (props) => {
 
       <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_with_proxy_integration')}</h2>
 
+      {/* TODO delete tmp props */}
       <CustomBotWithProxyIntegrationCard
+        siteName="GROWI"
+        slackWSNameInWithProxy="SlackWorkSpaceName"
         isSlackScopeSet
       />
 

+ 14 - 9
src/client/js/components/Admin/SlackIntegration/CustomBotWithProxySettingsAccordion.jsx

@@ -8,6 +8,8 @@ const CustomBotWithProxySettingsAccordion = () => {
   const [connectionErrorCode, setConnectionErrorCode] = useState(null);
   const [connectionErrorMessage, setConnectionErrorMessage] = useState(null);
   const [connectionSuccessMessage, setConnectionSuccessMessage] = useState(null);
+  // TODO: get url
+  const [url, setUrl] = useState('http://localhost:3000/');
 
   const { t } = useTranslation();
 
@@ -72,10 +74,10 @@ const CustomBotWithProxySettingsAccordion = () => {
         </div>
       </Accordion>
       <Accordion
-        title={<><span className="mr-2">③</span>アクセストークンの発行 / GROWI Official Bot Proxy サービスへの登録</>}
+        title={<><span className="mr-2">③</span>{t('admin:slack_integration.accordion.generate_access_token')} / {t('admin:slack_integration.accordion.register_for_growi_official_bot_proxy_service')}</>}
       >
         <div className="py-4 px-5">
-          <p className="font-weight-bold">1. Access Tokenの発行</p>
+          <p className="font-weight-bold">1. {t('admin:slack_integration.accordion.generate_access_token')}</p>
           <div className="form-group row">
             <label className="text-left text-md-right col-md-3 col-form-label">Access Token for GROWI</label>
             <div className="col-md-6">
@@ -101,15 +103,18 @@ const CustomBotWithProxySettingsAccordion = () => {
               <button type="button" className="btn btn-primary mx-2">{ t('Update') }</button>
             </div>
           </div>
-          <p className="font-weight-bold">2. GROWI Official Bot Proxy サービスへの登録</p>
+          <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">
             <ol className="p-0">
-              <li><p className="ml-2">Slack上で`/growi register`と打つ</p></li>
+              <li><p className="ml-2">{t('admin:slack_integration.accordion.enter_growi_register_on_slack')}</p></li>
               {/* TODO: Copy to clipboard on click */}
               <li>
-                <p className="ml-2"><b>GROWI URL</b>には`http://localhost:3000/`<i className="fa fa-clipboard mx-1 text-secondary" aria-hidden="true"></i>を貼り付ける</p>
+                <p
+                  className="ml-2"
+                  dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.accordion.paste_growi_url') }}
+                />
               </li>
-              <li><p className="ml-2">上記で発行した<b>Access Token for GROWI と Access Token for Proxy</b>を入れる</p></li>
+              <li><p className="ml-2">{t('admin:slack_integration.accordion.enter_access_token_for_growi_and_proxy')}</p></li>
             </ol>
             {/* TODO: Insert photo */}
             <div className="rounded border w-50 d-flex justify-content-center align-items-center" style={{ height: '15rem' }}>
@@ -119,10 +124,10 @@ const CustomBotWithProxySettingsAccordion = () => {
         </div>
       </Accordion>
       <Accordion
-        title={<><span className="mr-2">④</span>ProxyのURLをGROWIに登録する</>}
+        title={<><span className="mr-2">④</span>{t('admin:slack_integration.accordion.set_proxy_url_on_growi')}</>}
       >
         <div className="p-4">
-          <p className="text-center">Slack上に通知された<b>Proxy URL</b>を入力し、更新してください。</p>
+          <p className="text-center">{t('admin:slack_integration.accordion.enter_proxy_url_and_update')}</p>
           <div className="form-group row my-4">
             <label className="text-left text-md-right col-md-3 col-form-label">Proxy URL</label>
             <div className="col-md-6">
@@ -140,7 +145,7 @@ const CustomBotWithProxySettingsAccordion = () => {
         </div>
       </Accordion>
       <Accordion
-        title={<><span className="mr-2">⑤</span>連携状況のテストをする</>}
+        title={<><span className="mr-2">⑤</span>{t('admin:slack_integration.accordion.test_connection')}</>}
       >
         {/* TODO: Responsive */}
         <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>

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

@@ -12,36 +12,36 @@ const CustomBotWithoutProxyIntegrationCard = (props) => {
         <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 && (
-          <div className="card slack-work-space-name-card">
-            <div className="m-2 text-center">
-              <h5 className="font-weight-bold">{props.slackWSNameInWithoutProxy}</h5>
-              <img width={20} height={20} src="/images/slack-integration/growi-bot-kun-icon.png" />
+            <div className="card slack-work-space-name-card">
+              <div className="m-2 text-center">
+                <h5 className="font-weight-bold">{props.slackWSNameInWithoutProxy}</h5>
+                <img width={20} height={20} src="/images/slack-integration/growi-bot-kun-icon.png" />
+              </div>
             </div>
-          </div>
-            )}
+          )}
         </div>
       </div>
 
       <div className="text-center w-25">
         {props.isSlackScopeSet && (
-        <div className="mt-5">
-          <p className="text-success small">
-            <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 className="mt-5">
+            <p className="text-success small">
+              <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>
+        )}
         {!props.isSlackScopeSet && (
-        <div className="mt-4">
-          <small
-            className="text-secondary m-0"
-                // eslint-disable-next-line react/no-danger
-            dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
-          />
-          <hr className="align-self-center admin-border-danger border-danger"></hr>
-        </div>
-          )}
+          <div className="mt-4">
+            <small
+              className="text-secondary m-0"
+                  // eslint-disable-next-line react/no-danger
+              dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.integration_sentence.integration_is_not_complete') }}
+            />
+            <hr className="align-self-center admin-border-danger border-danger"></hr>
+          </div>
+        )}
       </div>
 
       <div className="card rounded-lg shadow border-0 w-50 admin-bot-card mb-0">

+ 47 - 4
src/client/js/components/Admin/SlackIntegration/OfficialbotSettingsAccordion.jsx

@@ -11,8 +11,22 @@ const OfficialBotSettingsAccordion = () => {
       <Accordion
         title={<><span className="mr-2">①</span>{t('admin:slack_integration.accordion.install_bot_to_slack')}</>}
       >
-        {/* TODO: GW-5824 add accordion contents  */}
-        hoge
+        <div className="my-5 d-flex flex-column align-items-center">
+          {/* TODO: Insert install link */}
+          <button type="button" className="btn btn-primary text-nowrap" onClick={() => window.open('https://api.slack.com/apps', '_blank', 'noreferrer')}>
+            {t('admin:slack_integration.accordion.install_now')}
+            <i className="fa fa-external-link ml-2" aria-hidden="true" />
+          </button>
+          {/* TODO: Insert DOCS link */}
+          <a href="#">
+            <p className="text-center mt-1">
+              <small>
+                {t('admin:slack_integration.accordion.how_to_install')}
+                <i className="fa fa-external-link ml-2" aria-hidden="true" />
+              </small>
+            </p>
+          </a>
+        </div>
       </Accordion>
       <Accordion
         title={<><span className="mr-2">②</span>{t('admin:slack_integration.accordion.register_official_bot_proxy_service')}</>}
@@ -85,8 +99,37 @@ const OfficialBotSettingsAccordion = () => {
       <Accordion
         title={<><span className="mr-2">④</span>{t('admin:slack_integration.accordion.test_connection')}</>}
       >
-        {/* TODO: GW-5824 add accordion contents  */}
-        hoge
+        <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
+        <div className="d-flex justify-content-center">
+          <form className="form-row align-items-center w-25">
+            <div className="col-8 input-group-prepend">
+              <span className="input-group-text" id="slack-channel-addon"><i className="fa fa-hashtag" /></span>
+              <input
+                className="form-control w-100"
+                type="text"
+                placeholder="Slack Channel"
+              />
+            </div>
+            <div className="col-4">
+              <button
+                type="submit"
+                className="btn btn-info mx-3 font-weight-bold"
+              >Test
+              </button>
+            </div>
+          </form>
+        </div>
+        <form>
+          <div className="row my-3 justify-content-center">
+            <div className="form-group slack-connection-log w-25">
+              <label className="mb-1"><p className="border-info slack-connection-log-title pl-2">Logs</p></label>
+              <textarea
+                className="form-control card border-info slack-connection-log-body rounded-lg"
+                readOnly
+              />
+            </div>
+          </div>
+        </form>
       </Accordion>
     </div>
   );

+ 3 - 3
src/server/routes/admin.js

@@ -223,9 +223,9 @@ module.exports = function(crowi, app) {
     return res.render('admin/external-accounts');
   };
 
-  actions.legacySlackIntegration = {};
-  actions.legacySlackIntegration = function(req, res) {
-    return res.render('admin/legacy-slack-integration');
+  actions.slackIntegrationLegacy = {};
+  actions.slackIntegrationLegacy = function(req, res) {
+    return res.render('admin/slack-integration-legacy');
   };
 
   actions.slackIntegration = {};

+ 1 - 1
src/server/routes/apiv3/index.js

@@ -46,8 +46,8 @@ module.exports = (crowi) => {
   router.use('/bookmarks', require('./bookmarks')(crowi));
   router.use('/attachment', require('./attachment')(crowi));
 
-  router.use('/slack-bot', require('./slack-bot')(crowi));
   router.use('/slack-integration', require('./slack-integration')(crowi));
+  router.use('/slack-integration-legacy', require('./slack-integration-legacy')(crowi));
   router.use('/staffs', require('./staffs')(crowi));
 
   return router;

+ 0 - 135
src/server/routes/apiv3/slack-bot.js

@@ -1,135 +0,0 @@
-const express = require('express');
-
-const loggerFactory = require('@alias/logger');
-
-const logger = loggerFactory('growi:routes:apiv3:slack-bot');
-
-const router = express.Router();
-const { verificationSlackRequest } = require('@growi/slack');
-
-module.exports = (crowi) => {
-  this.app = crowi.express;
-
-  // Check if the access token is correct
-  function verificationAccessToken(req, res, next) {
-    const botType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
-    if (botType === 'customBotWithoutProxy') {
-      return next();
-    }
-    const slackBotAccessToken = req.body.slack_bot_access_token || null;
-
-    if (slackBotAccessToken == null || slackBotAccessToken !== this.crowi.configManager.getConfig('crowi', 'slackbot:access-token')) {
-      logger.error('slack_bot_access_token is invalid.');
-      return res.send('*Access token is inValid*');
-    }
-
-    return next();
-  }
-
-  function verificationRequestUrl(req, res, next) {
-    // for verification request URL on Event Subscriptions
-    if (req.body.type === 'url_verification') {
-      return res.send(req.body);
-    }
-
-    return next();
-  }
-
-  const addSlackBotSigningSecret = (req, res, next) => {
-    req.signingSecret = crowi.configManager.getConfig('crowi', 'slackbot:signingSecret');
-    return next();
-  };
-
-  router.post('/commands', verificationRequestUrl, addSlackBotSigningSecret, verificationSlackRequest, verificationAccessToken, async(req, res) => {
-
-    // Send response immediately to avoid opelation_timeout error
-    // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
-    res.send();
-
-    const { body } = req;
-    const args = body.text.split(' ');
-    const command = args[0];
-
-    try {
-      switch (command) {
-        case 'search':
-          await crowi.slackBotService.showEphemeralSearchResults(body, args);
-          break;
-        case 'create':
-          await crowi.slackBotService.createModal(body);
-          break;
-        default:
-          await crowi.slackBotService.notCommand(body);
-          break;
-      }
-    }
-    catch (error) {
-      logger.error(error);
-      return res.send(error.message);
-    }
-  });
-
-  const handleBlockActions = async(payload) => {
-    const { action_id: actionId } = payload.actions[0];
-
-    switch (actionId) {
-      case 'shareSearchResults': {
-        await crowi.slackBotService.shareSearchResults(payload);
-        break;
-      }
-      case 'showNextResults': {
-        const parsedValue = JSON.parse(payload.actions[0].value);
-
-        const { body, args, offset } = parsedValue;
-        const newOffset = offset + 10;
-        await crowi.slackBotService.showEphemeralSearchResults(body, args, newOffset);
-        break;
-      }
-      default:
-        break;
-    }
-  };
-
-  const handleViewSubmission = async(payload) => {
-    const { callback_id: callbackId } = payload.view;
-
-    switch (callbackId) {
-      case 'createPage':
-        await crowi.slackBotService.createPageInGrowi(payload);
-        break;
-      default:
-        break;
-    }
-  };
-
-  router.post('/interactions', verificationRequestUrl, addSlackBotSigningSecret, verificationSlackRequest, async(req, res) => {
-
-    // Send response immediately to avoid opelation_timeout error
-    // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
-    res.send();
-
-    const payload = JSON.parse(req.body.payload);
-    const { type } = payload;
-
-    try {
-      switch (type) {
-        case 'block_actions':
-          await handleBlockActions(payload);
-          break;
-        case 'view_submission':
-          await handleViewSubmission(payload);
-          break;
-        default:
-          break;
-      }
-    }
-    catch (error) {
-      logger.error(error);
-      return res.send(error.message);
-    }
-
-  });
-
-
-  return router;
-};

+ 354 - 0
src/server/routes/apiv3/slack-integration-legacy.js

@@ -0,0 +1,354 @@
+const loggerFactory = require('@alias/logger');
+
+const logger = loggerFactory('growi:routes:apiv3:notification-setting');
+const express = require('express');
+const { body } = require('express-validator');
+const crypto = require('crypto');
+const { WebClient, LogLevel } = require('@slack/web-api');
+const ErrorV3 = require('../../models/vo/error-apiv3');
+
+const router = express.Router();
+
+/**
+ * @swagger
+ *  tags:
+ *    name: SlackIntegration
+ */
+
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      CustomBotWithoutProxy:
+ *        description: CustomBotWithoutProxy
+ *        type: object
+ *        properties:
+ *          slackSigningSecret:
+ *            type: string
+ *          slackBotToken:
+ *            type: string
+ *          currentBotType:
+ *            type: string
+ *      SlackIntegration:
+ *        description: SlackIntegration
+ *        type: object
+ *        properties:
+ *          currentBotType:
+ *            type: string
+ */
+
+
+module.exports = (crowi) => {
+  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
+  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const adminRequired = require('../../middlewares/admin-required')(crowi);
+  const csrf = require('../../middlewares/csrf')(crowi);
+  const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
+
+  const validator = {
+    CustomBotWithoutProxy: [
+      body('slackSigningSecret').isString(),
+      body('slackBotToken').isString(),
+      body('currentBotType').isString(),
+    ],
+    SlackIntegration: [
+      body('currentBotType')
+        .isIn(['officialBot', 'customBotWithoutProxy', 'customBotWithProxy']),
+    ],
+    NotificationTestToSlackWorkSpace: [
+      body('channel').trim().not().isEmpty()
+        .isString(),
+    ],
+  };
+
+  async function updateSlackBotSettings(params) {
+    const { configManager } = crowi;
+    // update config without publishing S2sMessage
+    return configManager.updateConfigsInTheSameNamespace('crowi', params, true);
+  }
+
+
+  function generateAccessToken(user) {
+    const hasher = crypto.createHash('sha512');
+    hasher.update(new Date().getTime() + user._id);
+
+    return hasher.digest('base64');
+  }
+
+  /**
+   * @swagger
+   *
+   *    /slack-integration/:
+   *      get:
+   *        tags: [SlackBotSettingParams]
+   *        operationId: getSlackBotSettingParams
+   *        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 = {
+      accessToken: crowi.configManager.getConfig('crowi', 'slackbot:access-token'),
+      currentBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
+      // TODO impl when creating official bot
+      officialBotSettings: {
+        // TODO impl this after GW-4939
+        // AccessToken: "tempaccessdatahogehoge",
+      },
+      customBotWithoutProxySettings: {
+        // TODO impl this after GW-4939
+        // AccessToken: "tempaccessdatahogehoge",
+        slackSigningSecretEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:signingSecret'),
+        slackBotTokenEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:token'),
+        slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
+        slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
+        isConnectedToSlack: crowi.slackBotService.isConnectedToSlack,
+      },
+      // TODO imple when creating with proxy
+      customBotWithProxySettings: {
+        // TODO impl this after GW-4939
+        // AccessToken: "tempaccessdatahogehoge",
+      },
+    };
+    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 slack service
+        await crowi.slackBotService.initialize();
+        crowi.slackBotService.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'), 500);
+      }
+    });
+
+  /**
+   * @swagger
+   *
+   *    /slack-integration/custom-bot-without-proxy/:
+   *      put:
+   *        tags: [CustomBotWithoutProxy]
+   *        operationId: putCustomBotWithoutProxy
+   *        summary: /slack-integration/custom-bot-without-proxy
+   *        description: Put customBotWithoutProxy setting.
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/CustomBotWithoutProxy'
+   *        responses:
+   *           200:
+   *             description: Succeeded to put CustomBotWithoutProxy setting.
+   */
+  router.put('/custom-bot-without-proxy',
+    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:currentBotType': currentBotType,
+      };
+      try {
+        await updateSlackBotSettings(requestParams);
+
+        // initialize slack service
+        await crowi.slackBotService.initialize();
+        crowi.slackBotService.publishUpdatedMessage();
+
+        // TODO Impl to delete AccessToken both of Proxy and GROWI when botType changes.
+        const customBotWithoutProxySettingParams = {
+          slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
+          slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
+          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'), 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', loginRequiredStrictly, adminRequired, async(req, res) => {
+
+    try {
+      const slackWorkSpaceName = await crowi.slackBotService.getSlackChannelName();
+      return res.apiv3({ slackWorkSpaceName });
+    }
+    catch (error) {
+      let msg = 'Error occured in slack_bot_token';
+      if (error.data.error === 'missing_scope') {
+        msg = 'missing_scope';
+      }
+      logger.error('Error', error);
+      return res.apiv3Err(new ErrorV3(msg, 'get-SlackWorkSpaceName-failed'), 500);
+    }
+
+  });
+
+  /**
+   * @swagger
+   *
+   *    /slack-integration/access-token:
+   *      put:
+   *        tags: [SlackIntegration]
+   *        operationId: getCustomBotSetting
+   *        summary: /slack-integration
+   *        description: Generate accessToken
+   *        responses:
+   *          200:
+   *            description: Succeeded to update access token for slack
+   */
+  router.put('/access-token', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+
+    try {
+      const accessToken = generateAccessToken(req.user);
+      await updateSlackBotSettings({ 'slackbot:access-token': accessToken });
+
+      // initialize slack service
+      await crowi.slackBotService.initialize();
+      crowi.slackBotService.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'), 500);
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /slack-integration/test-notification-to-slack-work-space:
+   *      post:
+   *        tags: [SlackTestToWorkSpace]
+   *        operationId: postSlackMessageToSlackWorkSpace
+   *        summary: test to send message to slack work space
+   *        description: post message to slack work space
+   *        responses:
+   *          200:
+   *            description: Succeeded to send a message to slack work space
+   */
+  router.post('/notification-test-to-slack-work-space',
+    loginRequiredStrictly, adminRequired, csrf, validator.NotificationTestToSlackWorkSpace, apiV3FormValidator, async(req, res) => {
+      const isConnectedToSlack = crowi.slackBotService.isConnectedToSlack;
+      const { channel } = req.body;
+
+      if (isConnectedToSlack === false) {
+        const msg = 'Bot User OAuth Token is not setup.';
+        logger.error('Error', msg);
+        return res.apiv3Err(new ErrorV3(msg, 'not-setup-slack-bot-token', 400));
+      }
+
+      const slackBotToken = crowi.configManager.getConfig('crowi', 'slackbot:token');
+      this.client = new WebClient(slackBotToken, { logLevel: LogLevel.DEBUG });
+      logger.debug('SlackBot: setup is done');
+
+      try {
+        await this.client.chat.postMessage({
+          channel: `#${channel}`,
+          text: 'Your test was successful!',
+        });
+        logger.info(`SlackTest: send success massage to slack work space at #${channel}.`);
+        logger.info(`If you do not receive a message, you may not have invited the bot to the #${channel} channel.`);
+        // eslint-disable-next-line max-len
+        const message = `Successfully send message to Slack work space. See #general channel. If you do not receive a message, you may not have invited the bot to the #${channel} channel.`;
+        return res.apiv3({ message });
+      }
+      catch (error) {
+        const msg = `Error: ${error.data.error}. Needed:${error.data.needed}`;
+        logger.error('Error', error);
+        return res.apiv3Err(new ErrorV3(msg, 'notification-test-slack-work-space-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 slack service
+      await crowi.slackBotService.initialize();
+      crowi.slackBotService.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);
+    }
+  });
+
+  return router;
+};

+ 121 - 312
src/server/routes/apiv3/slack-integration.js

@@ -1,353 +1,162 @@
+const express = require('express');
+
 const loggerFactory = require('@alias/logger');
 
-const logger = loggerFactory('growi:routes:apiv3:notification-setting');
-const express = require('express');
-const { body } = require('express-validator');
-const crypto = require('crypto');
-const { WebClient, LogLevel } = require('@slack/web-api');
-const ErrorV3 = require('../../models/vo/error-apiv3');
+const { verifySlackRequest } = require('@growi/slack');
 
+const logger = loggerFactory('growi:routes:apiv3:slack-integration');
 const router = express.Router();
 
-/**
- * @swagger
- *  tags:
- *    name: SlackIntegration
- */
-
-/**
- * @swagger
- *
- *  components:
- *    schemas:
- *      CustomBotWithoutProxy:
- *        description: CustomBotWithoutProxy
- *        type: object
- *        properties:
- *          slackSigningSecret:
- *            type: string
- *          slackBotToken:
- *            type: string
- *          currentBotType:
- *            type: string
- *      SlackIntegration:
- *        description: SlackIntegration
- *        type: object
- *        properties:
- *          currentBotType:
- *            type: string
- */
+module.exports = (crowi) => {
+  this.app = crowi.express;
 
+  const { configManager } = crowi;
 
-module.exports = (crowi) => {
-  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
-  const adminRequired = require('../../middlewares/admin-required')(crowi);
-  const csrf = require('../../middlewares/csrf')(crowi);
-  const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
-
-  const validator = {
-    CustomBotWithoutProxy: [
-      body('slackSigningSecret').isString(),
-      body('slackBotToken').isString(),
-      body('currentBotType').isString(),
-    ],
-    SlackIntegration: [
-      body('currentBotType')
-        .isIn(['officialBot', 'customBotWithoutProxy', 'customBotWithProxy']),
-    ],
-    NotificationTestToSlackWorkSpace: [
-      body('channel').trim().not().isEmpty()
-        .isString(),
-    ],
-  };
+  // Check if the access token is correct
+  function verifyAccessTokenFromProxy(req, res, next) {
+    const { body } = req;
+    const { tokenPtoG } = body;
 
-  async function updateSlackBotSettings(params) {
-    const { configManager } = crowi;
-    // update config without publishing S2sMessage
-    return configManager.updateConfigsInTheSameNamespace('crowi', params, true);
-  }
+    const correctToken = configManager.getConfig('crowi', 'slackbot:access-token');
 
+    logger.debug('verifyAccessTokenFromProxy', {
+      tokenPtoG,
+      correctToken,
+    });
 
-  function generateAccessToken(user) {
-    const hasher = crypto.createHash('sha512');
-    hasher.update(new Date().getTime() + user._id);
+    if (tokenPtoG == null || tokenPtoG !== correctToken) {
+      return res.status(403).send({ message: 'The access token that identifies the request source is slackbot-proxy is invalid.' });
+    }
 
-    return hasher.digest('base64');
+    next();
   }
 
-  /**
-   * @swagger
-   *
-   *    /slack-integration/:
-   *      get:
-   *        tags: [SlackBotSettingParams]
-   *        operationId: getSlackBotSettingParams
-   *        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 = {
-      accessToken: crowi.configManager.getConfig('crowi', 'slackbot:access-token'),
-      currentBotType: crowi.configManager.getConfig('crowi', 'slackbot:currentBotType'),
-      // TODO impl when creating official bot
-      officialBotSettings: {
-        // TODO impl this after GW-4939
-        // AccessToken: "tempaccessdatahogehoge",
-      },
-      customBotWithoutProxySettings: {
-        // TODO impl this after GW-4939
-        // AccessToken: "tempaccessdatahogehoge",
-        slackSigningSecretEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:signingSecret'),
-        slackBotTokenEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:token'),
-        slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
-        slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
-        isConnectedToSlack: crowi.slackBotService.isConnectedToSlack,
-      },
-      // TODO imple when creating with proxy
-      customBotWithProxySettings: {
-        // TODO impl this after GW-4939
-        // AccessToken: "tempaccessdatahogehoge",
-      },
-    };
-    return res.apiv3({ slackBotSettingParams });
-  });
+  const addSigningSecretToReq = (req, res, next) => {
+    req.slackSigningSecret = configManager.getConfig('crowi', 'slackbot:signingSecret');
+    return next();
+  };
 
-  /**
-   * @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 slack service
-        await crowi.slackBotService.initialize();
-        crowi.slackBotService.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'), 500);
-      }
-    });
+  async function handleCommands(req, res) {
+    const { body } = req;
 
-  /**
-   * @swagger
-   *
-   *    /slack-integration/custom-bot-without-proxy/:
-   *      put:
-   *        tags: [CustomBotWithoutProxy]
-   *        operationId: putCustomBotWithoutProxy
-   *        summary: /slack-integration/custom-bot-without-proxy
-   *        description: Put customBotWithoutProxy setting.
-   *        requestBody:
-   *          required: true
-   *          content:
-   *            application/json:
-   *              schema:
-   *                $ref: '#/components/schemas/CustomBotWithoutProxy'
-   *        responses:
-   *           200:
-   *             description: Succeeded to put CustomBotWithoutProxy setting.
-   */
-  router.put('/custom-bot-without-proxy',
-    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:currentBotType': currentBotType,
-      };
-      try {
-        await updateSlackBotSettings(requestParams);
-
-        // initialize slack service
-        await crowi.slackBotService.initialize();
-        crowi.slackBotService.publishUpdatedMessage();
-
-        // TODO Impl to delete AccessToken both of Proxy and GROWI when botType changes.
-        const customBotWithoutProxySettingParams = {
-          slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
-          slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
-          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'), 500);
-      }
-    });
+    if (body.text == null) {
+      return 'No text.';
+    }
 
-  /**
-   * @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', loginRequiredStrictly, adminRequired, async(req, res) => {
+    /*
+     * TODO: use parseSlashCommand
+     */
+
+    // Send response immediately to avoid opelation_timeout error
+    // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
+    res.send();
+
+    const args = body.text.split(' ');
+    const command = args[0];
 
     try {
-      const slackWorkSpaceName = await crowi.slackBotService.getSlackChannelName();
-      return res.apiv3({ slackWorkSpaceName });
+      switch (command) {
+        case 'search':
+          await crowi.slackBotService.showEphemeralSearchResults(body, args);
+          break;
+        case 'create':
+          await crowi.slackBotService.createModal(body);
+          break;
+        default:
+          await crowi.slackBotService.notCommand(body);
+          break;
+      }
     }
     catch (error) {
-      let msg = 'Error occured in slack_bot_token';
-      if (error.data.error === 'missing_scope') {
-        msg = 'missing_scope';
-      }
-      logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(msg, 'get-SlackWorkSpaceName-failed'), 500);
+      logger.error(error);
+      return res.send(error.message);
     }
+  }
 
+  router.post('/commands', addSigningSecretToReq, verifySlackRequest, async(req, res) => {
+    return handleCommands(req, res);
   });
 
-  /**
-   * @swagger
-   *
-   *    /slack-integration/access-token:
-   *      put:
-   *        tags: [SlackIntegration]
-   *        operationId: getCustomBotSetting
-   *        summary: /slack-integration
-   *        description: Generate accessToken
-   *        responses:
-   *          200:
-   *            description: Succeeded to update access token for slack
-   */
-  router.put('/access-token', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+  router.post('/proxied/commands', verifyAccessTokenFromProxy, async(req, res) => {
+    const { body } = req;
 
-    try {
-      const accessToken = generateAccessToken(req.user);
-      await updateSlackBotSettings({ 'slackbot:access-token': accessToken });
-
-      // initialize slack service
-      await crowi.slackBotService.initialize();
-      crowi.slackBotService.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'), 500);
+    // eslint-disable-next-line max-len
+    // see: https://api.slack.com/apis/connections/events-api#the-events-api__subscribing-to-event-types__events-api-request-urls__request-url-configuration--verification
+    if (body.type === 'url_verification') {
+      return body.challenge;
     }
+
+    return handleCommands(req, res);
   });
 
-  /**
-   * @swagger
-   *
-   *    /slack-integration/test-notification-to-slack-work-space:
-   *      post:
-   *        tags: [SlackTestToWorkSpace]
-   *        operationId: postSlackMessageToSlackWorkSpace
-   *        summary: test to send message to slack work space
-   *        description: post message to slack work space
-   *        responses:
-   *          200:
-   *            description: Succeeded to send a message to slack work space
-   */
-  router.post('/notification-test-to-slack-work-space',
-    loginRequiredStrictly, adminRequired, csrf, validator.NotificationTestToSlackWorkSpace, apiV3FormValidator, async(req, res) => {
-      const isConnectedToSlack = crowi.slackBotService.isConnectedToSlack;
-      const { channel } = req.body;
-
-      if (isConnectedToSlack === false) {
-        const msg = 'Bot User OAuth Token is not setup.';
-        logger.error('Error', msg);
-        return res.apiv3Err(new ErrorV3(msg, 'not-setup-slack-bot-token', 400));
-      }
 
-      const slackBotToken = crowi.configManager.getConfig('crowi', 'slackbot:token');
-      this.client = new WebClient(slackBotToken, { logLevel: LogLevel.DEBUG });
-      logger.debug('SlackBot: setup is done');
-
-      try {
-        await this.client.chat.postMessage({
-          channel: `#${channel}`,
-          text: 'Your test was successful!',
-        });
-        logger.info(`SlackTest: send success massage to slack work space at #${channel}.`);
-        logger.info(`If you do not receive a message, you may not have invited the bot to the #${channel} channel.`);
-        // eslint-disable-next-line max-len
-        const message = `Successfully send message to Slack work space. See #general channel. If you do not receive a message, you may not have invited the bot to the #${channel} channel.`;
-        return res.apiv3({ message });
+  const handleBlockActions = async(payload) => {
+    const { action_id: actionId } = payload.actions[0];
+
+    switch (actionId) {
+      case 'shareSearchResults': {
+        await crowi.slackBotService.shareSearchResults(payload);
+        break;
       }
-      catch (error) {
-        const msg = `Error: ${error.data.error}. Needed:${error.data.needed}`;
-        logger.error('Error', error);
-        return res.apiv3Err(new ErrorV3(msg, 'notification-test-slack-work-space-failed'), 500);
+      case 'showNextResults': {
+        const parsedValue = JSON.parse(payload.actions[0].value);
+
+        const { body, args, offset } = parsedValue;
+        const newOffset = offset + 10;
+        await crowi.slackBotService.showEphemeralSearchResults(body, args, newOffset);
+        break;
       }
-    });
+      default:
+        break;
+    }
+  };
 
-  /**
-   * @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) => {
+  const handleViewSubmission = async(payload) => {
+    const { callback_id: callbackId } = payload.view;
 
-    try {
-      await updateSlackBotSettings({ 'slackbot:access-token': null });
+    switch (callbackId) {
+      case 'createPage':
+        await crowi.slackBotService.createPageInGrowi(payload);
+        break;
+      default:
+        break;
+    }
+  };
+
+  async function handleInteractions(req, res) {
 
-      // initialize slack service
-      await crowi.slackBotService.initialize();
-      crowi.slackBotService.publishUpdatedMessage();
+    // Send response immediately to avoid opelation_timeout error
+    // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
+    res.send();
 
-      return res.apiv3({});
+    const payload = JSON.parse(req.body.payload);
+    const { type } = payload;
+
+    try {
+      switch (type) {
+        case 'block_actions':
+          await handleBlockActions(payload);
+          break;
+        case 'view_submission':
+          await handleViewSubmission(payload);
+          break;
+        default:
+          break;
+      }
     }
     catch (error) {
-      const msg = 'Error occured in discard of slackbotAccessToken';
-      logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(msg, 'discard-slackbotAccessToken-failed'), 500);
+      logger.error(error);
+      return res.send(error.message);
     }
+
+  }
+
+  router.post('/interactions', addSigningSecretToReq, verifySlackRequest, async(req, res) => {
+    return handleInteractions(req, res);
+  });
+
+  router.post('/proxied/interactions', verifyAccessTokenFromProxy, async(req, res) => {
+    return handleInteractions(req, res);
   });
 
   return router;

+ 1 - 1
src/server/routes/index.js

@@ -93,7 +93,7 @@ module.exports = function(crowi, app) {
   app.get('/admin/notification/slackSetting/disconnect' , loginRequiredStrictly , adminRequired , admin.notification.disconnectFromSlack);
   app.get('/admin/global-notification/new'              , loginRequiredStrictly , adminRequired , admin.globalNotification.detail);
   app.get('/admin/global-notification/:id'              , loginRequiredStrictly , adminRequired , admin.globalNotification.detail);
-  app.get('/admin/legacy-slack-integration'         , loginRequiredStrictly , adminRequired,  admin.legacySlackIntegration);
+  app.get('/admin/slack-integration-legacy'             , loginRequiredStrictly , adminRequired,  admin.slackIntegrationLegacy);
   app.get('/admin/slack-integration'                    , loginRequiredStrictly , adminRequired,  admin.slackIntegration);
 
   app.get('/admin/users'                                , loginRequiredStrictly , adminRequired , admin.user.index);

+ 0 - 0
src/server/views/admin/legacy-slack-integration.html → src/server/views/admin/slack-integration-legacy.html