Переглянути джерело

Merge branch 'feat/growi-bot' into fix/immediately-reply-check-mark

zahmis 5 роки тому
батько
коміт
4fa29c241a

+ 5 - 4
packages/app-for-hoisting/package.json

@@ -2,8 +2,7 @@
   "name": "@growi/app-for-hoisting",
   "version": "0.9.0-RC",
   "license": "MIT",
-  "scripts": {
-  },
+  "scripts": {},
   "// comments for dependencies": {
     "openid-client": "Node.js 12 or higher is required for openid-client@3 and above."
   },
@@ -21,7 +20,8 @@
     "aws-sdk": "^2.88.0",
     "axios": "^0.21.1",
     "body-parser": "^1.18.2",
-    "bunyan": "^1.8.12",
+    "browser-bunyan": "^1.6.3",
+    "bunyan": "^1.8.15",
     "bunyan-format": "^0.2.1",
     "check-node-version": "^4.1.0",
     "connect-flash": "~0.1.1",
@@ -89,6 +89,7 @@
     "string-width": "^4.1.0",
     "swig-templates": "^2.0.2",
     "uglifycss": "^0.0.29",
+    "universal-bunyan": "^0.9.2",
     "unzipper": "^0.10.5",
     "url-join": "^4.0.0",
     "validator": "^12.0.0",
@@ -120,7 +121,7 @@
     "babel-plugin-lodash": "^3.3.4",
     "babel-plugin-transform-imports": "^2.0.0",
     "bootstrap": "^4.5.0",
-    "browser-bunyan": "^1.3.0",
+    "browser-bunyan": "^1.6.3",
     "browser-sync": "^2.26.3",
     "bunyan-debug": "^2.0.0",
     "cli": "~1.0.1",

+ 5 - 4
packages/app/package.json

@@ -2,8 +2,7 @@
   "name": "@growi/app",
   "version": "0.9.0-RC",
   "license": "MIT",
-  "scripts": {
-  },
+  "scripts": {},
   "// comments for dependencies": {
     "openid-client": "Node.js 12 or higher is required for openid-client@3 and above."
   },
@@ -21,7 +20,8 @@
     "aws-sdk": "^2.88.0",
     "axios": "^0.21.1",
     "body-parser": "^1.18.2",
-    "bunyan": "^1.8.12",
+    "browser-bunyan": "^1.6.3",
+    "bunyan": "^1.8.15",
     "bunyan-format": "^0.2.1",
     "check-node-version": "^4.1.0",
     "connect-flash": "~0.1.1",
@@ -89,6 +89,7 @@
     "string-width": "^4.1.0",
     "swig-templates": "^2.0.2",
     "uglifycss": "^0.0.29",
+    "universal-bunyan": "^0.9.2",
     "unzipper": "^0.10.5",
     "url-join": "^4.0.0",
     "validator": "^12.0.0",
@@ -120,7 +121,7 @@
     "babel-plugin-lodash": "^3.3.4",
     "babel-plugin-transform-imports": "^2.0.0",
     "bootstrap": "^4.5.0",
-    "browser-bunyan": "^1.3.0",
+    "browser-bunyan": "^1.6.3",
     "browser-sync": "^2.26.3",
     "bunyan-debug": "^2.0.0",
     "cli": "~1.0.1",

+ 5 - 1
packages/slack/package.json

@@ -15,13 +15,17 @@
     "test:lint:fix": "eslint src --ext .ts --fix"
   },
   "dependencies": {
-    "dotenv-flow": "^3.2.0"
+    "browser-bunyan": "^1.6.3",
+    "bunyan": "^1.8.15",
+    "dotenv-flow": "^3.2.0",
+    "universal-bunyan": "^0.9.2"
   },
   "devDependencies": {
     "@slack/bolt": "^3.3.0",
     "@types/jest": "^26.0.22",
     "@typescript-eslint/eslint-plugin": "^4.18.0",
     "@typescript-eslint/parser": "^4.18.0",
+    "browser-bunyan": "^1.6.3",
     "cross-env": "^7.0.0",
     "eslint-import-resolver-typescript": "^2.4.0",
     "eslint-plugin-jest": "^24.3.2",

+ 6 - 1
packages/slackbot-proxy/package.json

@@ -26,13 +26,17 @@
     "@tsed/platform-express": "^6.43.0",
     "@tsed/swagger": "^6.43.0",
     "@tsed/typeorm": "^6.43.0",
+    "axios": "^0.21.1",
+    "browser-bunyan": "^1.6.3",
+    "bunyan": "^1.8.15",
     "compression": "^1.7.4",
     "cookie-parser": "^1.4.5",
     "cross-env": "^7.0.0",
     "dotenv-flow": "^3.2.0",
     "method-override": "^3.0.0",
     "mysql2": "^2.2.5",
-    "typeorm": "^0.2.31"
+    "typeorm": "^0.2.31",
+    "universal-bunyan": "^0.9.2"
   },
   "devDependencies": {
     "@tsed/core": "^6.43.0",
@@ -41,6 +45,7 @@
     "@tsed/schema": "^6.43.0",
     "@typescript-eslint/eslint-plugin": "^4.18.0",
     "@typescript-eslint/parser": "^4.18.0",
+    "browser-bunyan": "^1.6.3",
     "eslint-import-resolver-typescript": "^2.4.0",
     "ts-jest": "^26.5.4",
     "ts-node": "^9.1.1",

+ 16 - 0
packages/slackbot-proxy/src/config/logger/config.dev.ts

@@ -0,0 +1,16 @@
+import { UniversalBunyanConfig } from 'universal-bunyan';
+
+const config: UniversalBunyanConfig = {
+  default: 'info',
+
+  // 'express-session': 'debug',
+
+  /*
+   * configure level for server
+   */
+  // 'express:*': 'debug',
+  // 'slackbot-proxy:*': 'debug',
+
+};
+
+export default config;

+ 16 - 0
packages/slackbot-proxy/src/config/logger/config.prod.ts

@@ -0,0 +1,16 @@
+import { UniversalBunyanConfig } from 'universal-bunyan';
+
+const config: UniversalBunyanConfig = {
+  default: 'info',
+
+  // 'express-session': 'debug',
+
+  /*
+   * configure level for server
+   */
+  // 'express:*': 'debug',
+  // 'slackbot-proxy:*': 'debug',
+
+};
+
+export default config;

+ 75 - 35
packages/slackbot-proxy/src/controllers/slack.ts

@@ -1,6 +1,9 @@
 import {
-  BodyParams, Controller, Get, Inject, Post, Req, Res,
+  BodyParams, Controller, Get, Inject, Post, Req, Res, UseBefore,
 } from '@tsed/common';
+
+import axios from 'axios';
+
 import { parseSlashCommand } from '@growi/slack';
 import { Installation } from '~/entities/installation';
 
@@ -10,6 +13,12 @@ import { OrderRepository } from '~/repositories/order';
 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 {
@@ -29,10 +38,6 @@ export class SlackCtrl {
   @Inject()
   registerService: RegisterService;
 
-  growiCommandsMappings = {
-    register: async(body:{[key:string]:string}):Promise<void> => this.registerService.execSlashCommand(body),
-  };
-
   @Get('/testsave')
   testsave(): void {
     const installation = new Installation();
@@ -72,64 +77,99 @@ export class SlackCtrl {
       + '</a>';
   }
 
-  @Post('/events')
-  async handleEvent(@BodyParams() body:{[key:string]:string}, @Res() res: Res): Promise<string> {
-    // Send response immediately to avoid opelation_timeout error
-    // See https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events
+  @Post('/commands')
+  @UseBefore(AuthorizeCommandMiddleware)
+  async handleCommand(@Req() req: AuthedReq, @Res() res: Res): Promise<void|string> {
+    const { body, authorizeResult } = req;
 
     if (body.text == null) {
       return 'No text.';
     }
 
-    const parsedBody = parseSlashCommand(body);
-    const executeGrowiCommand = this.growiCommandsMappings[parsedBody.growiCommandType];
-
-    if (executeGrowiCommand == null) {
-      return 'No executeGrowiCommand';
-    }
-    await executeGrowiCommand(body);
+    // 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 installation = await this.installationRepository.findByID('1');
-    if (installation == null) {
-      throw new Error('installation is reqiured');
+    const growiCommand = parseSlashCommand(body);
+
+    // register
+    if (growiCommand.growiCommandType === 'register') {
+      await this.registerService.process(growiCommand, authorizeResult, body as {[key:string]:string});
+      return;
     }
 
-    // Find the latest order by installationId
-    let order = await this.orderRepository.findOne({
-      installation: installation.id,
-    }, {
-      order: {
-        createdAt: 'DESC',
-      },
+    /*
+     * forward to GROWI server
+     */
+    const installationId = authorizeResult.enterpriseId || authorizeResult.teamId;
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
+    const relations = await this.relationRepository.find({ installation: installation?.id });
+
+    await relations.map((relation: Relation) => {
+      // generate API URL
+      const url = new URL('/_api/v3/slack-bot/commands', relation.growiUri);
+      return axios.post(url.toString(), {
+        ...body,
+        tokenPtoG: relation.tokenPtoG,
+        growiCommand,
+      });
     });
+  }
+
+  @Post('/interactions')
+  @UseBefore(AuthorizeInteractionMiddleware)
+  async handleInteraction(@Req() req: AuthedReq, @Res() res: Res): Promise<void|string> {
+    logger.info('receive interaction', req.body);
+    logger.info('receive interaction', req.authorizeResult);
+    return;
+  }
 
-    if (order == null || order.isExpired()) {
-      order = await this.orderRepository.save({ installation: installation.id });
+  @Post('/events')
+  async handleEvent(@BodyParams() body:{[key:string]:string}, @Res() res: Res): Promise<void|string> {
+    // 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 'This action will be handled by bolt service.';
+    logger.info('receive event', body);
+
+    return;
   }
 
   @Get('/oauth_redirect')
   async handleOauthRedirect(@Req() req: Req, @Res() res: Res): Promise<void> {
 
-    // illegal state
-    // TODO: https://youtrack.weseek.co.jp/issue/GW-5543
-    if (req.query.state !== 'init') {
+    if (req.query.state === '') {
       res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
       res.end('<html>'
       + '<head><meta name="viewport" content="width=device-width,initial-scale=1"></head>'
       + '<body style="text-align:center; padding-top:20%;">'
       + '<h1>Illegal state, try it again.</h1>'
       + '<a href="/slack/install">'
-      + 'go to install page'
+      + 'Go to install page'
       + '</a>'
       + '</body></html>');
     }
 
-    this.installerService.installer.handleCallback(req, res, {
-      // success: (installation, metadata, req, res) => {},
+    await this.installerService.installer.handleCallback(req, res, {
+      success: (installation, metadata, req, res) => {
+        logger.info('Success to install', { installation, metadata });
+
+        const appPageUrl = `https://slack.com/apps/${installation.appId}`;
+
+        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
+        res.end('<html>'
+        + '<head><meta name="viewport" content="width=device-width,initial-scale=1"></head>'
+        + '<body style="text-align:center; padding-top:20%;">'
+        + '<h1>Congratulations!</h1>'
+        + '<p>GROWI Bot installation has succeeded.</p>'
+        + `<a href="${appPageUrl}">`
+        + 'Access to Slack App detail page.'
+        + '</a>'
+        + '</body></html>');
+      },
       failure: (error, installOptions, req, res) => {
         res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
         res.end('<html>'

+ 6 - 0
packages/slackbot-proxy/src/interfaces/authorized-req.ts

@@ -0,0 +1,6 @@
+import { AuthorizeResult } from '@slack/oauth';
+import { Req } from '@tsed/common';
+
+export type AuthedReq = Req & {
+  authorizeResult: AuthorizeResult,
+};

+ 6 - 0
packages/slackbot-proxy/src/interfaces/growi-command-processor.ts

@@ -0,0 +1,6 @@
+import { AuthorizeResult } from '@slack/oauth';
+import { GrowiCommand } from '@growi/slack';
+
+export interface GrowiCommandProcessor {
+  process(growiCommand: GrowiCommand, authorizeResult: AuthorizeResult, body: {[key:string]:string}): Promise<void>
+}

+ 0 - 3
packages/slackbot-proxy/src/interfaces/growi-commands-mappings.ts

@@ -1,3 +0,0 @@
-export interface GrowiCommandsMappings{
-  execSlashCommand(body:{[key:string]:string}):Promise<void>
-}

+ 93 - 0
packages/slackbot-proxy/src/middlewares/authorizer.ts

@@ -0,0 +1,93 @@
+import { InstallationQuery } from '@slack/oauth';
+import {
+  IMiddleware, Inject, Middleware, Req, Res,
+} from '@tsed/common';
+
+import { AuthedReq } from '~/interfaces/authorized-req';
+import { InstallationRepository } from '~/repositories/installation';
+import { InstallerService } from '~/services/InstallerService';
+
+@Middleware()
+export class AuthorizeCommandMiddleware implements IMiddleware {
+
+  @Inject()
+  installerService: InstallerService;
+
+  @Inject()
+  installationRepository: InstallationRepository;
+
+  async use(@Req() req: AuthedReq, @Res() res: Res): Promise<void> {
+    const { body } = req;
+
+    // extract id from body
+    const teamId = body.team_id;
+    const enterpriseId = body.enterprize_id;
+
+    if (teamId == null && enterpriseId == null) {
+      res.writeHead(400);
+      return res.end();
+    }
+
+    // create query from body
+    const query: InstallationQuery<boolean> = {
+      teamId,
+      enterpriseId,
+      isEnterpriseInstall: body.is_enterprise_install === 'true',
+    };
+
+    const result = await this.installerService.installer.authorize(query);
+
+    if (result.botToken == null) {
+      res.writeHead(403);
+      return res.end();
+    }
+
+    // set authorized data
+    req.authorizeResult = result;
+  }
+
+}
+
+
+@Middleware()
+export class AuthorizeInteractionMiddleware implements IMiddleware {
+
+  @Inject()
+  installerService: InstallerService;
+
+  @Inject()
+  installationRepository: InstallationRepository;
+
+  async use(@Req() req: AuthedReq, @Res() res: Res): Promise<void> {
+    const { body } = req;
+
+    const payload = JSON.parse(body.payload);
+
+    // extract id from body
+    const teamId = payload.team?.id;
+    const enterpriseId = body.enterprise?.id;
+
+    if (teamId == null && enterpriseId == null) {
+      res.writeHead(400);
+      return res.end();
+    }
+
+    // create query from body
+    const query: InstallationQuery<boolean> = {
+      teamId,
+      enterpriseId,
+      isEnterpriseInstall: body.is_enterprise_install === 'true',
+    };
+
+    const result = await this.installerService.installer.authorize(query);
+
+    if (result.botToken == null) {
+      res.writeHead(403);
+      return res.end();
+    }
+
+    // set authorized data
+    req.authorizeResult = result;
+  }
+
+}

+ 10 - 10
packages/slackbot-proxy/src/services/InstallerService.ts

@@ -55,16 +55,16 @@ export class InstallerService {
           return;
         },
         fetchInstallation: async(installQuery: InstallationQuery<boolean>) => {
-          const installation: SlackInstallation<'v1' | 'v2', boolean> = {
-            team: undefined,
-            enterprise: undefined,
-            user: {
-              id: '',
-              token: undefined,
-              scopes: undefined,
-            },
-          };
-          return installation;
+          const id = installQuery.enterpriseId || installQuery.teamId;
+
+          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+          const installation = await repository.findByTeamIdOrEnterpriseId(id!);
+
+          if (installation == null) {
+            throw new Error('Failed fetching installation');
+          }
+
+          return installation.data;
         },
       },
     });

+ 0 - 16
packages/slackbot-proxy/src/services/RecieveService.ts

@@ -1,16 +0,0 @@
-import { Service } from '@tsed/di';
-import { parseSlashCommand } from '@growi/slack';
-
-@Service()
-export class ReceiveService {
-
-  receiveContentsFromSlack(body:{[key:string]:string}) : string {
-    const parseBody = parseSlashCommand(body);
-    if (parseBody.growiCommandType === 'register') {
-      console.log('register action occured');
-      return 'register action occurd';
-    }
-    return 'return receiveContentsFromSlack';
-  }
-
-}

+ 10 - 5
packages/slackbot-proxy/src/services/RegisterService.ts

@@ -1,14 +1,19 @@
 import { Service } from '@tsed/di';
 import { WebClient, LogLevel } from '@slack/web-api';
-import { generateInputSectionBlock } from '@growi/slack';
-import { GrowiCommandsMappings } from '../interfaces/growi-commands-mappings';
+import { generateInputSectionBlock, GrowiCommand } from '@growi/slack';
+import { AuthorizeResult } from '@slack/oauth';
+
+import { GrowiCommandProcessor } from '~/interfaces/growi-command-processor';
 
 @Service()
-export class RegisterService implements GrowiCommandsMappings {
+export class RegisterService implements GrowiCommandProcessor {
+
+  async process(growiCommand: GrowiCommand, authorizeResult: AuthorizeResult, body: {[key:string]:string}): Promise<void> {
+
+    const { botToken } = authorizeResult;
 
-  async execSlashCommand(body:{[key:string]:string}):Promise<void> {
     // tmp use process.env
-    const client = new WebClient(process.env.SLACK_BOT_USER_OAUTH_TOKEN, { logLevel: LogLevel.DEBUG });
+    const client = new WebClient(botToken, { logLevel: LogLevel.DEBUG });
     await client.views.open({
       trigger_id: body.trigger_id,
       view: {

+ 17 - 0
packages/slackbot-proxy/src/utils/logger/index.ts

@@ -0,0 +1,17 @@
+import Logger from 'bunyan';
+import { createLogger } from 'universal-bunyan';
+
+import configForDev from '~/config/logger/config.dev';
+import configForProd from '~/config/logger/config.prod';
+
+const isProduction = process.env.NODE_ENV === 'production';
+const config = isProduction ? configForProd : configForDev;
+
+const loggerFactory = function(name: string): Logger {
+  return createLogger({
+    name,
+    config,
+  });
+};
+
+export default loggerFactory;

+ 36 - 0
public/images/slack-integration/slackbot-difficulty-level-easy.svg

@@ -0,0 +1,36 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 60 60">
+  <defs>
+    <style>
+      .cls-1, .cls-4 {
+        fill: none;
+      }
+
+      .cls-1 {
+        stroke: #81d5b8;
+        stroke-width: 3px;
+      }
+
+      .cls-2 {
+        font-family: NotoSansCJKjp-Bold, Noto Sans CJK JP;
+        font-size: 16px;
+        font-weight: 700;
+        fill: #81d5b8;
+      }
+
+      .cls-3 {
+        stroke: none;
+      }
+    </style>
+  </defs>
+  <g id="Group_4361" data-name="Group 4361" transform="translate(-71.69 -49.04)">
+    <g id="Group_4358" data-name="Group 4358">
+      <g id="Group_4357" data-name="Group 4357">
+        <g id="Ellipse_97" data-name="Ellipse 97" class="cls-1" transform="translate(71.69 49.04)">
+          <circle class="cls-3" cx="30" cy="30" r="30"/>
+          <circle class="cls-4" cx="30" cy="30" r="28.5"/>
+        </g>
+        <text id="EASY" class="cls-2" transform="translate(83.884 89.632) rotate(-11)"><tspan x="0" y="0">EASY</tspan></text>
+      </g>
+    </g>
+  </g>
+</svg>

+ 32 - 0
public/images/slack-integration/slackbot-difficulty-level-hard.svg

@@ -0,0 +1,32 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 60 60">
+  <defs>
+    <style>
+      .cls-1, .cls-4 {
+        fill: none;
+      }
+
+      .cls-1 {
+        stroke: #ff8080;
+        stroke-width: 3px;
+      }
+
+      .cls-2 {
+        fill: #ff8080;
+        font-size: 16px;
+        font-family: NotoSansCJKjp-Bold, Noto Sans CJK JP;
+        font-weight: 700;
+      }
+
+      .cls-3 {
+        stroke: none;
+      }
+    </style>
+  </defs>
+  <g id="Group_4360" data-name="Group 4360" transform="translate(-303.305 -23.039)">
+    <g id="Ellipse_99" data-name="Ellipse 99" class="cls-1" transform="translate(303.305 23.039)">
+      <circle class="cls-3" cx="30" cy="30" r="30"/>
+      <circle class="cls-4" cx="30" cy="30" r="28.5"/>
+    </g>
+    <text id="HARD" class="cls-2" transform="translate(312.93 64.277) rotate(-11)"><tspan x="0" y="0">HARD</tspan></text>
+  </g>
+</svg>

+ 32 - 0
public/images/slack-integration/slackbot-difficulty-level-normal.svg

@@ -0,0 +1,32 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 60 60">
+  <defs>
+    <style>
+      .cls-1, .cls-4 {
+        fill: none;
+      }
+
+      .cls-1 {
+        stroke: #ffce60;
+        stroke-width: 3px;
+      }
+
+      .cls-2 {
+        fill: #ffce60;
+        font-size: 11px;
+        font-family: NotoSansCJKjp-Bold, Noto Sans CJK JP;
+        font-weight: 700;
+      }
+
+      .cls-3 {
+        stroke: none;
+      }
+    </style>
+  </defs>
+  <g id="Group_4359" data-name="Group 4359" transform="translate(-163.69 -44.039)">
+    <g id="Ellipse_98" data-name="Ellipse 98" class="cls-1" transform="translate(163.69 44.039)">
+      <circle class="cls-3" cx="30" cy="30" r="30"/>
+      <circle class="cls-4" cx="30" cy="30" r="28.5"/>
+    </g>
+    <text id="NORMAL" class="cls-2" transform="translate(171.836 83.813) rotate(-11)"><tspan x="0" y="0">NORMAL</tspan></text>
+  </g>
+</svg>

+ 1 - 7
src/client/js/components/Admin/SlackIntegration/BotTypeCard.jsx

@@ -67,13 +67,7 @@ const BotTypeCard = (props) => {
       <div className="card-body p-4">
         <div className="card-text">
           <div className="my-2">
-            <div className="d-flex justify-content-between mb-3">
-              {/* TODO add image of difficulties by GW-5638
-               <span>{t('admin:slack_integration.selecting_bot_types.set_up')}</span>
-               <span className={`bot-type-disc-${value.setUp}`}>{t(`admin:slack_integration.selecting_bot_types.${value.setUp}`)}</span>  */}
-
-
-            </div>
+            <img className="d-block mx-auto mb-4" src={`/images/slack-integration/slackbot-difficulty-level-${botDetails[props.botType].setUp}.svg`}></img>
             <div className="d-flex justify-content-between mb-3">
               <span>{t('admin:slack_integration.selecting_bot_types.multiple_workspaces_integration')}</span>
               <img className="bot-type-disc" src={`/images/slack-integration/${botDetails[props.botType].multiWSIntegration}.png`} alt="" />

+ 60 - 59
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx

@@ -25,67 +25,68 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
   };
 
   return (
-    <div className="card-body">
-      <table className="table settings-table">
-        <colgroup>
-          <col className="item-name" />
-          <col className="from-db" />
-          <col className="from-env-vars" />
-        </colgroup>
-        <thead>
-          <tr><th className="border-top-0"></th><th className="border-top-0">Database</th><th className="border-top-0">Environment variables</th></tr>
-        </thead>
-        <tbody>
-          <tr>
-            <th>Signing Secret</th>
-            <td>
-              <input
-                className="form-control"
-                type="text"
-                value={props.slackSigningSecret || ''}
-                onChange={e => onChangeSigningSecretHandler(e.target.value)}
-              />
-            </td>
-            <td>
-              <input
-                className="form-control"
-                type="text"
-                value={props.slackSigningSecretEnv || ''}
-                readOnly
-              />
-              <p className="form-text text-muted">
-                {/* eslint-disable-next-line react/no-danger */}
-                <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACK_SIGNING_SECRET' }) }} />
-              </p>
-            </td>
-          </tr>
-          <tr>
-            <th>Bot User OAuth Token</th>
-            <td>
-              <input
-                className="form-control"
-                type="text"
-                value={props.slackBotToken || ''}
-                onChange={e => onChangeBotTokenHandler(e.target.value)}
-              />
-            </td>
-            <td>
-              <input
-                className="form-control"
-                type="text"
-                value={props.slackBotTokenEnv || ''}
-                readOnly
-              />
-              <p className="form-text text-muted">
-                {/* eslint-disable-next-line react/no-danger */}
-                <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACK_BOT_TOKEN' }) }} />
-              </p>
-            </td>
-          </tr>
-        </tbody>
-      </table>
+    <div className="w-75 mx-auto">
+
+      <h3>Signing Secret</h3>
+      <div className="row">
+
+        <div className="col-sm">
+          <p>Database</p>
+          <input
+            className="form-control"
+            type="text"
+            value={props.slackSigningSecret || ''}
+            onChange={e => onChangeSigningSecretHandler(e.target.value)}
+          />
+        </div>
+
+        <div className="col-sm">
+          <p>Environment variables</p>
+          <input
+            className="form-control"
+            type="text"
+            value={props.slackSigningSecretEnv || ''}
+            readOnly
+          />
+          <p className="form-text text-muted">
+            {/* eslint-disable-next-line react/no-danger */}
+            <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACK_SIGNING_SECRET' }) }} />
+          </p>
+        </div>
+
+      </div>
+
+      <h3>Bot User OAuth Token</h3>
+      <div className="row">
+
+        <div className="col-sm">
+          <p>Database</p>
+          <input
+            className="form-control"
+            type="text"
+            value={props.slackBotToken || ''}
+            onChange={e => onChangeBotTokenHandler(e.target.value)}
+          />
+        </div>
+
+        <div className="col-sm">
+          <p>Environment variables</p>
+          <input
+            className="form-control"
+            type="text"
+            value={props.slackBotTokenEnv || ''}
+            readOnly
+          />
+          <p className="form-text text-muted">
+            {/* eslint-disable-next-line react/no-danger */}
+            <small dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.use_env_var_if_empty', { variable: 'SLACK_BOT_TOKEN' }) }} />
+          </p>
+        </div>
+
+      </div>
 
       <AdminUpdateButtonRow onClick={updateSecretTokenHandler} disabled={false} />
+
     </div>
   );
 };

+ 2 - 2
src/server/routes/apiv3/slack-bot.js

@@ -36,7 +36,7 @@ module.exports = (crowi) => {
     return next();
   };
 
-  router.post('/', verificationRequestUrl, addSlackBotSigningSecret, verificationSlackRequest, verificationAccessToken, async(req, res) => {
+  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
@@ -98,7 +98,7 @@ module.exports = (crowi) => {
     }
   };
 
-  router.post('/interactive', verificationRequestUrl, addSlackBotSigningSecret, verificationSlackRequest, async(req, res) => {
+  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

+ 24 - 1
yarn.lock

@@ -4679,7 +4679,7 @@ brorand@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
 
-browser-bunyan@^1.3.0:
+browser-bunyan@^1.6.3:
   version "1.6.3"
   resolved "https://registry.yarnpkg.com/browser-bunyan/-/browser-bunyan-1.6.3.tgz#0e58c51ff48507317ba8e5cf579e8b6bad7281e0"
   integrity sha512-HRg+acpwO3dsY2RWgtjw2wPVHV+uzbCrdhUxD25+qo5NFSTpbfJekrRP0yFNypAhG5LwXFV1Dc5FIc8cxwU5rQ==
@@ -4950,6 +4950,16 @@ bunyan@^1.8.12, bunyan@^1.8.3:
     mv "~2"
     safe-json-stringify "~1"
 
+bunyan@^1.8.15:
+  version "1.8.15"
+  resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.15.tgz#8ce34ca908a17d0776576ca1b2f6cbd916e93b46"
+  integrity sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==
+  optionalDependencies:
+    dtrace-provider "~0.8"
+    moment "^2.19.3"
+    mv "~2"
+    safe-json-stringify "~1"
+
 busboy@^0.2.11:
   version "0.2.14"
   resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453"
@@ -12726,6 +12736,11 @@ moment@>=2.26.0:
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
   integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
 
+moment@^2.19.3:
+  version "2.29.1"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
+  integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
+
 mongodb@3.6.2, mongodb@^3.1.0, mongodb@^3.6.2:
   version "3.6.2"
   resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.6.2.tgz#1154a4ac107bf1375112d83a29c5cf97704e96b6"
@@ -19215,6 +19230,14 @@ unist-util-visit@^1.1.0:
   dependencies:
     unist-util-visit-parents "^2.0.0"
 
+universal-bunyan@^0.9.2:
+  version "0.9.2"
+  resolved "https://registry.yarnpkg.com/universal-bunyan/-/universal-bunyan-0.9.2.tgz#4cf09dc34070390d8f5df4fe9af6a80fcd0dd574"
+  integrity sha512-MkyO17+5AVCpFfhMtYLODvSZmPxV8eHcoOAWobEXXzlXrSnf5YgCV5lBWcMV3VPaaKyZPQ0oG5PSWYmGSBGtIg==
+  dependencies:
+    bunyan-format "^0.2.1"
+    minimatch "^3.0.4"
+
 universal-user-agent@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"