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

Merge branch 'feat/growi-bot' into imprv/switch-display-by-passing-test

zahmis 4 лет назад
Родитель
Сommit
57ddeeb07f

+ 8 - 0
packages/slackbot-proxy/package.json

@@ -17,7 +17,11 @@
     "test:lint:fix": "eslint src --ext .ts --fix",
     "test:lint:fix": "eslint src --ext .ts --fix",
     "version": "node -p \"require('./package.json').version\""
     "version": "node -p \"require('./package.json').version\""
   },
   },
+  "// comments for dependencies": {
+    "express-graceful-exit": "0.5.2 includes a typings file error: https://github.com/emostar/express-graceful-exit/issues/24"
+  },
   "dependencies": {
   "dependencies": {
+    "@godaddy/terminus": "^4.8.0",
     "@growi/slack": "^0.9.0-RC",
     "@growi/slack": "^0.9.0-RC",
     "@slack/oauth": "^2.0.1",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.1.0",
     "@slack/web-api": "^6.1.0",
@@ -33,6 +37,9 @@
     "cookie-parser": "^1.4.5",
     "cookie-parser": "^1.4.5",
     "cross-env": "^7.0.0",
     "cross-env": "^7.0.0",
     "dotenv-flow": "^3.2.0",
     "dotenv-flow": "^3.2.0",
+    "express-bunyan-logger": "^1.3.3",
+    "express-graceful-exit": "=0.5.0",
+    "helmet": "^4.6.0",
     "method-override": "^3.0.0",
     "method-override": "^3.0.0",
     "mysql2": "^2.2.5",
     "mysql2": "^2.2.5",
     "typeorm": "^0.2.31",
     "typeorm": "^0.2.31",
@@ -47,6 +54,7 @@
     "@typescript-eslint/parser": "^4.18.0",
     "@typescript-eslint/parser": "^4.18.0",
     "browser-bunyan": "^1.6.3",
     "browser-bunyan": "^1.6.3",
     "eslint-import-resolver-typescript": "^2.4.0",
     "eslint-import-resolver-typescript": "^2.4.0",
+    "morgan": "^1.10.0",
     "ts-jest": "^26.5.4",
     "ts-jest": "^26.5.4",
     "ts-node": "^9.1.1",
     "ts-node": "^9.1.1",
     "ts-node-dev": "^1.1.6",
     "ts-node-dev": "^1.1.6",

+ 83 - 14
packages/slackbot-proxy/src/Server.ts

@@ -1,16 +1,30 @@
 import { Configuration, Inject, InjectorService } from '@tsed/di';
 import { Configuration, Inject, InjectorService } from '@tsed/di';
-import { PlatformApplication } from '@tsed/common';
-import '@tsed/platform-express'; // /!\ keep this import
+import { HttpServer, PlatformApplication } from '@tsed/common';
+import '@tsed/platform-express'; // !! DO NOT MODIFY !!
+import '@tsed/typeorm'; // !! DO NOT MODIFY !! -- https://github.com/tsedio/tsed/issues/1332#issuecomment-837840612
+import '@tsed/swagger';
+
 import bodyParser from 'body-parser';
 import bodyParser from 'body-parser';
 import compress from 'compression';
 import compress from 'compression';
 import cookieParser from 'cookie-parser';
 import cookieParser from 'cookie-parser';
 import methodOverride from 'method-override';
 import methodOverride from 'method-override';
-import '@tsed/swagger';
-import { TypeORMService } from '@tsed/typeorm';
+import helmet from 'helmet';
+import { Express } from 'express';
+import expressBunyanLogger from 'express-bunyan-logger';
+import gracefulExit from 'express-graceful-exit';
+
 import { ConnectionOptions } from 'typeorm';
 import { ConnectionOptions } from 'typeorm';
+import { createTerminus } from '@godaddy/terminus';
 
 
+import swaggerSettingsForDev from '~/config/swagger/config.dev';
+import swaggerSettingsForProd from '~/config/swagger/config.prod';
+import loggerFactory from '~/utils/logger';
 
 
 export const rootDir = __dirname;
 export const rootDir = __dirname;
+const isProduction = process.env.NODE_ENV === 'production';
+
+const logger = loggerFactory('slackbot-proxy:server');
+
 
 
 const connectionOptions: ConnectionOptions = {
 const connectionOptions: ConnectionOptions = {
   // The 'name' property must be set. Otherwise, the 'name' will be '0' and won't work well. -- 2021.04.05 Yuki Takei
   // The 'name' property must be set. Otherwise, the 'name' will be '0' and won't work well. -- 2021.04.05 Yuki Takei
@@ -25,18 +39,34 @@ const connectionOptions: ConnectionOptions = {
   synchronize: true,
   synchronize: true,
 } as ConnectionOptions;
 } as ConnectionOptions;
 
 
+const swaggerSettings = isProduction ? swaggerSettingsForProd : swaggerSettingsForDev;
+const helmetOptions = isProduction ? {} : {
+  contentSecurityPolicy: {
+    directives: {
+      defaultSrc: ['\'self\''],
+      styleSrc: ['\'self\'', '\'unsafe-inline\''],
+      imgSrc: ['\'self\'', 'data:', 'validator.swagger.io'],
+      scriptSrc: ['\'self\'', 'https: \'unsafe-inline\''],
+    },
+  },
+};
 
 
 @Configuration({
 @Configuration({
   rootDir,
   rootDir,
   acceptMimes: ['application/json'],
   acceptMimes: ['application/json'],
   httpPort: process.env.PORT || 8080,
   httpPort: process.env.PORT || 8080,
   httpsPort: false, // CHANGE
   httpsPort: false, // CHANGE
+  // disable RequestLogger of @tsed/logger
+  logger: { logRequest: false },
   mount: {
   mount: {
     '/': [
     '/': [
       `${rootDir}/controllers/*.ts`,
       `${rootDir}/controllers/*.ts`,
       `${rootDir}/middlewares/*.ts`,
       `${rootDir}/middlewares/*.ts`,
     ],
     ],
   },
   },
+  middlewares: [
+    helmet(helmetOptions),
+  ],
   componentsScan: [
   componentsScan: [
     `${rootDir}/services/*.ts`,
     `${rootDir}/services/*.ts`,
   ],
   ],
@@ -54,12 +84,7 @@ const connectionOptions: ConnectionOptions = {
       ],
       ],
     } as ConnectionOptions,
     } as ConnectionOptions,
   ],
   ],
-  swagger: [
-    {
-      path: '/docs',
-      specVersion: '3.0.1',
-    },
-  ],
+  swagger: swaggerSettings,
   exclude: [
   exclude: [
     '**/*.spec.ts',
     '**/*.spec.ts',
   ],
   ],
@@ -67,7 +92,7 @@ const connectionOptions: ConnectionOptions = {
 export class Server {
 export class Server {
 
 
   @Inject()
   @Inject()
-  app: PlatformApplication;
+  app: PlatformApplication<Express>;
 
 
   @Configuration()
   @Configuration()
   settings: Configuration;
   settings: Configuration;
@@ -81,10 +106,19 @@ export class Server {
     if (serverUri === undefined) {
     if (serverUri === undefined) {
       throw new Error('The environment variable \'SERVER_URI\' must be defined.');
       throw new Error('The environment variable \'SERVER_URI\' must be defined.');
     }
     }
+
+    const server = this.injector.get<HttpServer>(HttpServer);
+
+    // init express-graceful-exit
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    gracefulExit.init(server!);
   }
   }
 
 
   $beforeRoutesInit(): void {
   $beforeRoutesInit(): void {
+    const expressApp = this.app.getApp();
+
     this.app
     this.app
+      .use(gracefulExit.middleware(expressApp))
       .use(cookieParser())
       .use(cookieParser())
       .use(compress({}))
       .use(compress({}))
       .use(methodOverride())
       .use(methodOverride())
@@ -92,11 +126,46 @@ export class Server {
       .use(bodyParser.urlencoded({
       .use(bodyParser.urlencoded({
         extended: true,
         extended: true,
       }));
       }));
+
+    this.setupLogger();
   }
   }
 
 
-  async $onReady(): Promise<void> {
-    // for synchromizing when boot
-    this.injector.get<TypeORMService>(TypeORMService);
+  $beforeListen(): void {
+    const expressApp = this.app.getApp();
+    const server = this.injector.get<HttpServer>(HttpServer);
+
+    // init terminus
+    createTerminus(server, {
+      onSignal: async() => {
+        logger.info('server is starting cleanup');
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        gracefulExit.gracefulExitHandler(expressApp, server!);
+      },
+      onShutdown: async() => {
+        logger.info('cleanup finished, server is shutting down');
+      },
+    });
+  }
+
+  /**
+   * Setup logger for requests
+   */
+  private setupLogger(): void {
+    // use bunyan
+    if (isProduction) {
+      const logger = loggerFactory('express');
+
+      this.app.use(expressBunyanLogger({
+        logger,
+        excludes: ['*'],
+      }));
+    }
+    // use morgan
+    else {
+      // eslint-disable-next-line @typescript-eslint/no-var-requires
+      const morgan = require('morgan');
+      this.app.use(morgan('dev'));
+    }
   }
   }
 
 
 }
 }

+ 10 - 0
packages/slackbot-proxy/src/config/swagger/config.dev.ts

@@ -0,0 +1,10 @@
+import { SwaggerSettings } from '@tsed/swagger';
+
+const settings: SwaggerSettings[] = [
+  {
+    path: '/docs',
+    specVersion: '3.0.1',
+  },
+];
+
+export default settings;

+ 5 - 0
packages/slackbot-proxy/src/config/swagger/config.prod.ts

@@ -0,0 +1,5 @@
+import { SwaggerSettings } from '@tsed/swagger';
+
+const settings: SwaggerSettings[] = [];
+
+export default settings;

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

@@ -34,7 +34,7 @@ export class GrowiToSlackCtrl {
   @Inject()
   @Inject()
   orderRepository: OrderRepository;
   orderRepository: OrderRepository;
 
 
-  async requestToGrowi(growiUrl:string, proxyAccessToken:string):Promise<void> {
+  async requestToGrowi(growiUrl:string, tokenPtoG:string):Promise<void> {
     const url = new URL('/_api/v3/slack-integration/proxied/commands', growiUrl);
     const url = new URL('/_api/v3/slack-integration/proxied/commands', growiUrl);
     await axios.post(url.toString(), {
     await axios.post(url.toString(), {
       type: 'url_verification',
       type: 'url_verification',
@@ -42,7 +42,7 @@ export class GrowiToSlackCtrl {
     },
     },
     {
     {
       headers: {
       headers: {
-        'x-growi-ptog-tokens': proxyAccessToken,
+        'x-growi-ptog-tokens': tokenPtoG,
       },
       },
     });
     });
   }
   }
@@ -119,7 +119,7 @@ export class GrowiToSlackCtrl {
     // retrieve latest Order with Installation
     // retrieve latest Order with Installation
     const order = await this.orderRepository.createQueryBuilder('order')
     const order = await this.orderRepository.createQueryBuilder('order')
       .orderBy('order.createdAt', 'DESC')
       .orderBy('order.createdAt', 'DESC')
-      .where('proxyAccessToken = :token', { token: tokenGtoP })
+      .where('tokenGtoP = :token', { token: tokenGtoP })
       .leftJoinAndSelect('order.installation', 'installation')
       .leftJoinAndSelect('order.installation', 'installation')
       .getOne();
       .getOne();
 
 
@@ -129,7 +129,7 @@ export class GrowiToSlackCtrl {
 
 
     // Access the GROWI URL saved in the Order record and check if the GtoP token is valid.
     // Access the GROWI URL saved in the Order record and check if the GtoP token is valid.
     try {
     try {
-      await this.requestToGrowi(order.growiUrl, order.proxyAccessToken);
+      await this.requestToGrowi(order.growiUrl, order.tokenPtoG);
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
@@ -155,7 +155,7 @@ export class GrowiToSlackCtrl {
 
 
     // Transaction is not considered because it is used infrequently,
     // Transaction is not considered because it is used infrequently,
     const createdRelation = await this.relationRepository.save({
     const createdRelation = await this.relationRepository.save({
-      installation: order.installation, tokenGtoP: order.growiAccessToken, tokenPtoG: order.proxyAccessToken, growiUri: order.growiUrl,
+      installation: order.installation, tokenGtoP: order.tokenGtoP, tokenPtoG: order.tokenPtoG, growiUri: order.growiUrl,
     });
     });
 
 
     return res.send({ relation: createdRelation });
     return res.send({ relation: createdRelation });

+ 4 - 1
packages/slackbot-proxy/src/controllers/slack.ts

@@ -109,8 +109,11 @@ export class SlackCtrl {
       const url = new URL('/_api/v3/slack-integration/proxied/commands', relation.growiUri);
       const url = new URL('/_api/v3/slack-integration/proxied/commands', relation.growiUri);
       return axios.post(url.toString(), {
       return axios.post(url.toString(), {
         ...body,
         ...body,
-        tokenPtoG: relation.tokenPtoG,
         growiCommand,
         growiCommand,
+      }, {
+        headers: {
+          'x-growi-ptog-tokens': relation.tokenPtoG,
+        },
       });
       });
     });
     });
 
 

+ 2 - 2
packages/slackbot-proxy/src/entities/order.ts

@@ -25,10 +25,10 @@ export class Order {
   growiUrl: string;
   growiUrl: string;
 
 
   @Column()
   @Column()
-  growiAccessToken: string;
+  tokenGtoP: string;
 
 
   @Column()
   @Column()
-  proxyAccessToken: string;
+  tokenPtoG: string;
 
 
   isExpired():boolean {
   isExpired():boolean {
     const now = Date.now();
     const now = Date.now();

+ 7 - 7
packages/slackbot-proxy/src/services/RegisterService.ts

@@ -34,9 +34,9 @@ export class RegisterService implements GrowiCommandProcessor {
         private_metadata: JSON.stringify({ channel: body.channel_name }),
         private_metadata: JSON.stringify({ channel: body.channel_name }),
 
 
         blocks: [
         blocks: [
-          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.....'),
+          generateInputSectionBlock('growiUrl', 'GROWI domain', 'contents_input', false, 'https://example.com'),
+          generateInputSectionBlock('tokenPtoG', 'Access Token Proxy to GROWI', 'contents_input', false, 'jBMZvpk.....'),
+          generateInputSectionBlock('tokenGtoP', 'Access Token GROWI to Proxy', 'contents_input', false, 'sdg15av.....'),
         ],
         ],
       },
       },
     });
     });
@@ -47,12 +47,12 @@ export class RegisterService implements GrowiCommandProcessor {
       orderRepository: OrderRepository, installation: Installation | undefined, payload: any,
       orderRepository: OrderRepository, installation: Installation | undefined, payload: any,
   ): Promise<void> {
   ): Promise<void> {
     const inputValues = payload.view.state.values;
     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 growiUrl = inputValues.growiUrl.contents_input.value;
+    const tokenPtoG = inputValues.tokenPtoG.contents_input.value;
+    const tokenGtoP = inputValues.tokenGtoP.contents_input.value;
 
 
     orderRepository.save({
     orderRepository.save({
-      installation, growiUrl: inputGrowiUrl, growiAccessToken: inputGrowiAccessToken, proxyAccessToken: inputProxyAccessToken,
+      installation, growiUrl, tokenPtoG, tokenGtoP,
     });
     });
   }
   }
 
 

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

@@ -302,6 +302,7 @@
     "reset_all_settings": "Reset all settings",
     "reset_all_settings": "Reset all settings",
     "delete_slackbot_settings": "Reset Slack Bot settings",
     "delete_slackbot_settings": "Reset Slack Bot settings",
     "slackbot_settings_notice": "Reset",
     "slackbot_settings_notice": "Reset",
+    "all_settings_of_the_bot_will_be_reset": "All settings of the Bot will be reset.<br>Are you sure?",
     "accordion": {
     "accordion": {
       "create_bot": "Create Bot",
       "create_bot": "Create Bot",
       "how_to_create_a_bot": "How to create a bot",
       "how_to_create_a_bot": "How to create a bot",
@@ -312,7 +313,7 @@
       "register_for_growi_official_bot_proxy_service": "Register for GROWI Official Bot Proxy Service",
       "register_for_growi_official_bot_proxy_service": "Register for GROWI Official Bot Proxy Service",
       "enter_growi_register_on_slack": "Enter <b>/growi register</b> on slack",
       "enter_growi_register_on_slack": "Enter <b>/growi register</b> on slack",
       "paste_growi_url": "Since a modal is displayed, enter the following URL in <b>GROWI URL</b>.",
       "paste_growi_url": "Since a modal is displayed, enter the following URL in <b>GROWI URL</b>.",
-      "enter_access_token_for_growi_and_proxy": "Enter <b>Access Token for GROWI</b> and <b>Access Token for Proxy</b>",
+      "enter_access_token_for_growi_and_proxy": "Enter <b>Access Token Proxy to GROWI</b> and <b>Access Token GROWI to Proxy</b>",
       "set_proxy_url_on_growi": "Set Proxy URL on GROWI",
       "set_proxy_url_on_growi": "Set Proxy URL on GROWI",
       "copy_proxy_url": "1. When the above step ② are completed successfully, the Proxy URL will be displayed in the Slack Channel you selected in the modal, so copy it.",
       "copy_proxy_url": "1. When the above step ② are completed successfully, the Proxy URL will be displayed in the Slack Channel you selected in the modal, so copy it.",
       "enter_proxy_url_and_update": "2. Enter and update the Proxy URL that you copied in step ③ in the <b>Proxy URL</b>  of the <b>Custom bot with proxy integration</b> on this page.",
       "enter_proxy_url_and_update": "2. Enter and update the Proxy URL that you copied in step ③ in the <b>Proxy URL</b>  of the <b>Custom bot with proxy integration</b> on this page.",

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

@@ -299,6 +299,7 @@
     "reset_all_settings": "全ての設定をリセット",
     "reset_all_settings": "全ての設定をリセット",
     "delete_slackbot_settings": "Slack Bot 設定をリセットする",
     "delete_slackbot_settings": "Slack Bot 設定をリセットする",
     "slackbot_settings_notice": "リセットします",
     "slackbot_settings_notice": "リセットします",
+    "all_settings_of_the_bot_will_be_reset": "Botの全ての設定がリセットされます。<br>よろしいですか?",
     "accordion": {
     "accordion": {
       "create_bot": "Bot を作成する",
       "create_bot": "Bot を作成する",
       "how_to_create_a_bot": "作成手順はこちら",
       "how_to_create_a_bot": "作成手順はこちら",
@@ -309,7 +310,7 @@
       "register_for_growi_official_bot_proxy_service": "GROWI Official Bot Proxy サービスへの登録",
       "register_for_growi_official_bot_proxy_service": "GROWI Official Bot Proxy サービスへの登録",
       "enter_growi_register_on_slack": "Slack上で <b>/growi register</b> と打ちます。",
       "enter_growi_register_on_slack": "Slack上で <b>/growi register</b> と打ちます。",
       "paste_growi_url": "モーダルが表示されるので、<b>GROWI URL</b> には下記のURLを入力します。",
       "paste_growi_url": "モーダルが表示されるので、<b>GROWI URL</b> には下記のURLを入力します。",
-      "enter_access_token_for_growi_and_proxy": "上記で発行した<b>Access Token for GROWI</b> と <b>Access Token for Proxy</b>を入れる",
+      "enter_access_token_for_growi_and_proxy": "上記で発行した<b>Access Token Proxy to GROWI</b> と <b>Access Token GROWI to Proxy</b>を入れる",
       "set_proxy_url_on_growi": "ProxyのURLをGROWIに登録する",
       "set_proxy_url_on_growi": "ProxyのURLをGROWIに登録する",
       "copy_proxy_url": "1. ②が正常に完了すると、モーダル内で選択したSlack ChannelにProxy URLが表示されるので、コピーします。",
       "copy_proxy_url": "1. ②が正常に完了すると、モーダル内で選択したSlack ChannelにProxy URLが表示されるので、コピーします。",
       "enter_proxy_url_and_update": "2. 連携手順③でコピーしたProxy URLを、このページの<b>Custom bot with proxy 連携</b>の<b>Proxy URL</b>に入力、更新します。",
       "enter_proxy_url_and_update": "2. 連携手順③でコピーしたProxy URLを、このページの<b>Custom bot with proxy 連携</b>の<b>Proxy URL</b>に入力、更新します。",

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

@@ -309,6 +309,7 @@
     "reset_all_settings": "重置所有设置",
     "reset_all_settings": "重置所有设置",
     "delete_slackbot_settings": "重置 Slack Bot 设置",
     "delete_slackbot_settings": "重置 Slack Bot 设置",
     "slackbot_settings_notice": "重置",
     "slackbot_settings_notice": "重置",
+    "all_settings_of_the_bot_will_be_reset": "bot的所有设置将被重置。<br>你确定吗?",
     "accordion": {
     "accordion": {
       "create_bot": "创建 Bot",
       "create_bot": "创建 Bot",
       "how_to_create_a_bot": "如何创建一个 Bot",
       "how_to_create_a_bot": "如何创建一个 Bot",
@@ -319,7 +320,7 @@
       "register_for_growi_official_bot_proxy_service": "注册 GROWI Official Bot Proxy Service",
       "register_for_growi_official_bot_proxy_service": "注册 GROWI Official Bot Proxy Service",
       "enter_growi_register_on_slack": "在Slack中,输入 <b>/growi register</b>",
       "enter_growi_register_on_slack": "在Slack中,输入 <b>/growi register</b>",
       "paste_growi_url": "由于显示了模式,请在 <b>GROWI URL</b> 中输入以下URL",
       "paste_growi_url": "由于显示了模式,请在 <b>GROWI URL</b> 中输入以下URL",
-      "enter_access_token_for_growi_and_proxy": "插入上面发出的 <b>Access Token for GROWI</b> 和 <b>Access Token for Proxy</b>。",
+      "enter_access_token_for_growi_and_proxy": "插入上面发出的 <b>Access Token Proxy to GROWI</b> 和 <b>Access Token GROWI to Proxy</b>。",
       "set_proxy_url_on_growi": "向GROWI注册Proxy的URL",
       "set_proxy_url_on_growi": "向GROWI注册Proxy的URL",
       "copy_proxy_url": "1. 当上述步骤②成功完成后,Proxy URL将显示在你在模版中选择的Slack频道中,所以请复制它。",
       "copy_proxy_url": "1. 当上述步骤②成功完成后,Proxy URL将显示在你在模版中选择的Slack频道中,所以请复制它。",
       "enter_proxy_url_and_update": "2. 输入并更新你在步骤③中复制的ProxyURL到本页的<b>Custom bot with proxy 一体化</b>的<b>ProxyURL</b>。",
       "enter_proxy_url_and_update": "2. 输入并更新你在步骤③中复制的ProxyURL到本页的<b>Custom bot with proxy 一体化</b>的<b>ProxyURL</b>。",

+ 13 - 1
src/client/js/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.jsx

@@ -49,7 +49,19 @@ const DeleteSlackBotSettingsModal = React.memo((props) => {
         </span>
         </span>
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>
-        {t('admin:slack_integration.slackbot_settings_notice')}
+        {props.isResetAll && (
+          <>
+            <span
+              // eslint-disable-next-line react/no-danger
+              dangerouslySetInnerHTML={{ __html: t('admin:slack_integration.all_settings_of_the_bot_will_be_reset') }}
+            />
+          </>
+        )}
+        {!props.isResetAll && (
+          <>
+            {t('admin:slack_integration.slackbot_settings_notice')}
+          </>
+        )}
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
         <Button onClick={closeButtonHandler}>{t('Cancel')}</Button>
         <Button onClick={closeButtonHandler}>{t('Cancel')}</Button>

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

@@ -111,7 +111,7 @@ const OfficialBotSettings = (props) => {
         </div>
         </div>
       </div>
       </div>
 
 
-      <h2 className="admin-setting-header">{t('admin:slack_integration.official_bot_settings')}</h2>
+      <h2 className="admin-setting-header">{t('admin:slack_integration.integration_procedure')}</h2>
 
 
       <div className="mx-3">
       <div className="mx-3">
         {slackAppIntegrations.map((slackAppIntegration) => {
         {slackAppIntegrations.map((slackAppIntegration) => {

+ 4 - 6
src/client/js/components/Admin/SlackIntegration/WithProxyAccordions.jsx

@@ -89,12 +89,11 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers
     <div className="py-4 px-5">
     <div className="py-4 px-5">
       <p className="font-weight-bold">1. {t('admin:slack_integration.accordion.generate_access_token')}</p>
       <p className="font-weight-bold">1. {t('admin:slack_integration.accordion.generate_access_token')}</p>
       <div className="form-group row">
       <div className="form-group row">
-        <label className="text-left text-md-right col-md-3 col-form-label">Access Token for GROWI</label>
+        <label className="text-left text-md-right col-md-3 col-form-label">Access Token Proxy to GROWI</label>
         <div className="col-md-6">
         <div className="col-md-6">
           <div className="input-group-prepend mx-1">
           <div className="input-group-prepend mx-1">
-            {/* TODO: show tokenPtoG GW-5899 */}
             <input className="form-control" type="text" value={props.tokenPtoG || ''} readOnly />
             <input className="form-control" type="text" value={props.tokenPtoG || ''} readOnly />
-            <CopyToClipboard text="tokenPtoG" onCopy={() => toastSuccess(t('admin:slack_integration.copied_to_clipboard'))}>
+            <CopyToClipboard text={props.tokenPtoG || ''} onCopy={() => toastSuccess(t('admin:slack_integration.copied_to_clipboard'))}>
               <div className="btn input-group-text">
               <div className="btn input-group-text">
                 <i className="fa fa-clipboard mx-1" aria-hidden="true"></i>
                 <i className="fa fa-clipboard mx-1" aria-hidden="true"></i>
               </div>
               </div>
@@ -103,12 +102,11 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers
         </div>
         </div>
       </div>
       </div>
       <div className="form-group row">
       <div className="form-group row">
-        <label className="text-left text-md-right col-md-3 col-form-label">Access Token for Proxy</label>
+        <label className="text-left text-md-right col-md-3 col-form-label">Access Token GROWI to Proxy</label>
         <div className="col-md-6">
         <div className="col-md-6">
           <div className="input-group-prepend mx-1">
           <div className="input-group-prepend mx-1">
-            {/* TODO: show tokenGtoP GW-5899 */}
             <input className="form-control" type="text" value={props.tokenGtoP || ''} readOnly />
             <input className="form-control" type="text" value={props.tokenGtoP || ''} readOnly />
-            <CopyToClipboard text="tokenGtoP" onCopy={() => toastSuccess(t('admin:slack_integration.copied_to_clipboard'))}>
+            <CopyToClipboard text={props.tokenGtoP || ''} onCopy={() => toastSuccess(t('admin:slack_integration.copied_to_clipboard'))}>
               <div className="btn input-group-text">
               <div className="btn input-group-text">
                 <i className="fa fa-clipboard mx-1" aria-hidden="true"></i>
                 <i className="fa fa-clipboard mx-1" aria-hidden="true"></i>
               </div>
               </div>

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

@@ -55,6 +55,10 @@ module.exports = (crowi) => {
       body('currentBotType')
       body('currentBotType')
         .isIn(['officialBot', 'customBotWithoutProxy', 'customBotWithProxy']),
         .isIn(['officialBot', 'customBotWithoutProxy', 'customBotWithProxy']),
     ],
     ],
+    proxyUri: [
+      body('proxyUri').if(value => value !== '').trim().matches(/^(https?:\/\/)/)
+        .isURL({ require_tld: false }),
+    ],
     AccessTokens: [
     AccessTokens: [
       query('tokenGtoP').trim().not().isEmpty()
       query('tokenGtoP').trim().not().isEmpty()
         .isString()
         .isString()
@@ -77,7 +81,7 @@ module.exports = (crowi) => {
       'slackbot:currentBotType': null,
       'slackbot:currentBotType': null,
       'slackbot:signingSecret': null,
       'slackbot:signingSecret': null,
       'slackbot:token': null,
       'slackbot:token': null,
-      'slackbot:serverUri': null,
+      'slackbot:proxyServerUri': null,
     };
     };
     const { configManager } = crowi;
     const { configManager } = crowi;
     // update config without publishing S2sMessage
     // update config without publishing S2sMessage
@@ -92,7 +96,7 @@ module.exports = (crowi) => {
 
 
   async function getConnectionStatusesFromProxy(tokens) {
   async function getConnectionStatusesFromProxy(tokens) {
     const csv = tokens.join(',');
     const csv = tokens.join(',');
-    const proxyUri = crowi.configManager.getConfig('crowi', 'slackbot:serverUri');
+    const proxyUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyServerUri');
 
 
     const result = await axios.get(urljoin(proxyUri, '/g2s/connection-status'), {
     const result = await axios.get(urljoin(proxyUri, '/g2s/connection-status'), {
       headers: {
       headers: {
@@ -104,7 +108,7 @@ module.exports = (crowi) => {
   }
   }
 
 
   async function postRelationTest(token) {
   async function postRelationTest(token) {
-    const proxyUri = crowi.configManager.getConfig('crowi', 'slackbot:serverUri');
+    const proxyUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyServerUri');
 
 
     const result = await axios.get(urljoin(proxyUri, '/g2s/relation-test'), {
     const result = await axios.get(urljoin(proxyUri, '/g2s/relation-test'), {
       headers: {
       headers: {
@@ -142,8 +146,8 @@ module.exports = (crowi) => {
       settings.slackBotToken = configManager.getConfig('crowi', 'slackbot:token');
       settings.slackBotToken = configManager.getConfig('crowi', 'slackbot:token');
     }
     }
     else {
     else {
-      settings.proxyServerUri = crowi.configManager.getConfig('crowi', 'slackbot:serverUri');
-      settings.proxyUriEnvVars = configManager.getConfigFromEnvVars('crowi', 'slackbot:serverUri');
+      settings.proxyServerUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyServerUri');
+      settings.proxyUriEnvVars = configManager.getConfigFromEnvVars('crowi', 'slackbot:proxyServerUri');
     }
     }
 
 
     // retrieve connection statuses
     // retrieve connection statuses
@@ -416,10 +420,10 @@ module.exports = (crowi) => {
     }
     }
   });
   });
 
 
-  router.put('/proxy-uri', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
+  router.put('/proxy-uri', loginRequiredStrictly, adminRequired, csrf, validator.proxyUri, apiV3FormValidator, async(req, res) => {
     const { proxyUri } = req.body;
     const { proxyUri } = req.body;
 
 
-    const requestParams = { 'slackbot:serverUri': proxyUri };
+    const requestParams = { 'slackbot:proxyServerUri': proxyUri };
 
 
     try {
     try {
       await updateSlackBotSettings(requestParams);
       await updateSlackBotSettings(requestParams);

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

@@ -410,9 +410,9 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.STRING,
     type:    TYPES.STRING,
     default: null,
     default: null,
   },
   },
-  SERVER_URI: {
+  SLACK_INTEGRATION_PROXY_URI: {
     ns:      'crowi',
     ns:      'crowi',
-    key:     'slackbot:serverUri',
+    key:     'slackbot:proxyServerUri',
     type:    TYPES.STRING,
     type:    TYPES.STRING,
     default: null,
     default: null,
   },
   },

+ 47 - 0
yarn.lock

@@ -1433,6 +1433,13 @@
   resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
   resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
   integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
   integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
 
 
+"@godaddy/terminus@^4.8.0":
+  version "4.8.0"
+  resolved "https://registry.yarnpkg.com/@godaddy/terminus/-/terminus-4.8.0.tgz#7f1258abd731adcf5f08d8ff1aa0216a02d65062"
+  integrity sha512-C3u+LTmlhtqsk1Sjw9UDTAXfVngby62dJL71LyhpRcwX3FDeLb0yd1Rmxh1OjH5HouOr0IIuU4dhiJMT6NNXog==
+  dependencies:
+    stoppable "^1.1.0"
+
 "@google-cloud/common@^3.6.0":
 "@google-cloud/common@^3.6.0":
   version "3.6.0"
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-3.6.0.tgz#c2f6da5f79279a4a9ac7c71fc02d582beab98e8b"
   resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-3.6.0.tgz#c2f6da5f79279a4a9ac7c71fc02d582beab98e8b"
@@ -4368,6 +4375,13 @@ basic-auth@~2.0.0:
   dependencies:
   dependencies:
     safe-buffer "5.1.1"
     safe-buffer "5.1.1"
 
 
+basic-auth@~2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a"
+  integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==
+  dependencies:
+    safe-buffer "5.1.2"
+
 batch@0.6.1:
 batch@0.6.1:
   version "0.6.1"
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
   resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
@@ -7864,6 +7878,13 @@ express-form@~0.12.0:
     object-additions "^0.5.1"
     object-additions "^0.5.1"
     validator "^2.1.0"
     validator "^2.1.0"
 
 
+express-graceful-exit@=0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/express-graceful-exit/-/express-graceful-exit-0.5.0.tgz#fcde6495af84f361e66f119bf0e3eca69955f9f1"
+  integrity sha512-gZkxdmgYz6VVHjZiMTaO59N875ugmBnzopF95q8Fzmws86JLNWMwGjLVG4i+99WO4v9L5wl6vKYFyGoHKoNJ7A==
+  dependencies:
+    underscore "^1.4.4"
+
 express-session@^1.16.1:
 express-session@^1.16.1:
   version "1.16.1"
   version "1.16.1"
   resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.16.1.tgz#251ff9776c59382301de6c8c33411af357ed439c"
   resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.16.1.tgz#251ff9776c59382301de6c8c33411af357ed439c"
@@ -9352,6 +9373,11 @@ helmet@^3.13.0:
     referrer-policy "1.1.0"
     referrer-policy "1.1.0"
     x-xss-protection "1.1.0"
     x-xss-protection "1.1.0"
 
 
+helmet@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.6.0.tgz#579971196ba93c5978eb019e4e8ec0e50076b4df"
+  integrity sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==
+
 hex-color-regex@^1.1.0:
 hex-color-regex@^1.1.0:
   version "1.1.0"
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
   resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
@@ -12559,6 +12585,17 @@ mongoose@5.10.11:
     sift "7.0.1"
     sift "7.0.1"
     sliced "1.0.1"
     sliced "1.0.1"
 
 
+morgan@^1.10.0:
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7"
+  integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==
+  dependencies:
+    basic-auth "~2.0.1"
+    debug "2.6.9"
+    depd "~2.0.0"
+    on-finished "~2.3.0"
+    on-headers "~1.0.2"
+
 morgan@^1.9.0:
 morgan@^1.9.0:
   version "1.9.0"
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.9.0.tgz#d01fa6c65859b76fcf31b3cb53a3821a311d8051"
   resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.9.0.tgz#d01fa6c65859b76fcf31b3cb53a3821a311d8051"
@@ -17382,6 +17419,11 @@ sticky-events@^3.1.3:
   resolved "https://registry.yarnpkg.com/sticky-events/-/sticky-events-3.1.3.tgz#7b6b4091988b87b9f4e711c7c6532de07ab156dd"
   resolved "https://registry.yarnpkg.com/sticky-events/-/sticky-events-3.1.3.tgz#7b6b4091988b87b9f4e711c7c6532de07ab156dd"
   integrity sha512-nTm2bDaYTXFHAyQS59mWDRnnno/D8oj3C4JddOdipq6ZRnLLqjj+PeyCSbHPwMVdfvQoKwmMmAztp+YybDhvtA==
   integrity sha512-nTm2bDaYTXFHAyQS59mWDRnnno/D8oj3C4JddOdipq6ZRnLLqjj+PeyCSbHPwMVdfvQoKwmMmAztp+YybDhvtA==
 
 
+stoppable@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b"
+  integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==
+
 stream-browserify@^2.0.1:
 stream-browserify@^2.0.1:
   version "2.0.1"
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
   resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
@@ -18716,6 +18758,11 @@ umask@^1.1.0:
   resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d"
   resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d"
   integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=
   integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=
 
 
+underscore@^1.4.4:
+  version "1.13.1"
+  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1"
+  integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==
+
 unherit@^1.0.4:
 unherit@^1.0.4:
   version "1.1.2"
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.2.tgz#14f1f397253ee4ec95cec167762e77df83678449"
   resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.2.tgz#14f1f397253ee4ec95cec167762e77df83678449"