Explorar el Código

Merge branch 'feat/growi-bot' into imprv/create-reset-button

zahmis hace 4 años
padre
commit
0dc837ad86

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

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

+ 7 - 0
packages/slack/src/utils/webclient-factory.ts

@@ -0,0 +1,7 @@
+import { LogLevel, WebClient } from '@slack/web-api';
+
+const isProduction = process.env.NODE_ENV === 'production';
+
+export const generateWebClient = (botToken: string): WebClient => {
+  return new WebClient(botToken, { logLevel: isProduction ? LogLevel.DEBUG : LogLevel.INFO });
+};

+ 81 - 3
packages/slackbot-proxy/src/controllers/slack.ts

@@ -4,7 +4,7 @@ import {
 
 import axios from 'axios';
 
-import { parseSlashCommand } from '@growi/slack';
+import { generateMarkdownSectionBlock, generateWebClient, parseSlashCommand } from '@growi/slack';
 import { Installation } from '~/entities/installation';
 
 import { InstallationRepository } from '~/repositories/installation';
@@ -106,7 +106,7 @@ export class SlackCtrl {
     const installation = await this.installationRepository.findByTeamIdOrEnterpriseId(installationId!);
     const relations = await this.relationRepository.find({ installation: installation?.id });
 
-    await relations.map((relation: Relation) => {
+    const promises = relations.map((relation: Relation) => {
       // generate API URL
       const url = new URL('/_api/v3/slack-bot/commands', relation.growiUri);
       return axios.post(url.toString(), {
@@ -115,6 +115,32 @@ export class SlackCtrl {
         growiCommand,
       });
     });
+
+    // pickup PromiseRejectedResult only
+    const results = await Promise.allSettled(promises);
+    const rejectedResults: PromiseRejectedResult[] = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
+
+    if (rejectedResults.length > 0) {
+      const botToken = installation?.data.bot?.token;
+
+      // 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);
+      }
+    }
   }
 
   @Post('/interactions')
@@ -122,7 +148,59 @@ export class SlackCtrl {
   async handleInteraction(@Req() req: AuthedReq, @Res() res: Res): Promise<void|string> {
     logger.info('receive interaction', req.body);
     logger.info('receive interaction', req.authorizeResult);
-    return;
+
+    const { body, authorizeResult } = req;
+
+    // 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();
+
+    // pass
+    if (body.ssl_check != null) {
+      return;
+    }
+
+    const installationId = authorizeResult.enterpriseId || authorizeResult.teamId;
+    // 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);
+    }
+
   }
 
   @Post('/events')

+ 53 - 12
packages/slackbot-proxy/src/middlewares/authorizer.ts

@@ -1,11 +1,14 @@
-import { InstallationQuery } from '@slack/oauth';
+import { AuthorizeResult, InstallationQuery } from '@slack/oauth';
 import {
   IMiddleware, Inject, Middleware, Req, Res,
 } from '@tsed/common';
 
+import Logger from 'bunyan';
+
 import { AuthedReq } from '~/interfaces/authorized-req';
 import { InstallationRepository } from '~/repositories/installation';
 import { InstallerService } from '~/services/InstallerService';
+import loggerFactory from '~/utils/logger';
 
 @Middleware()
 export class AuthorizeCommandMiddleware implements IMiddleware {
@@ -16,15 +19,22 @@ export class AuthorizeCommandMiddleware implements IMiddleware {
   @Inject()
   installationRepository: InstallationRepository;
 
+  private logger: Logger;
+
+  constructor() {
+    this.logger = loggerFactory('slackbot-proxy:middlewares:AuthorizeCommandMiddleware');
+  }
+
   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;
+    const isEnterpriseInstall = body.is_enterprise_install === 'true';
 
     if (teamId == null && enterpriseId == null) {
-      res.writeHead(400);
+      res.writeHead(400, 'No installation found');
       return res.end();
     }
 
@@ -32,13 +42,22 @@ export class AuthorizeCommandMiddleware implements IMiddleware {
     const query: InstallationQuery<boolean> = {
       teamId,
       enterpriseId,
-      isEnterpriseInstall: body.is_enterprise_install === 'true',
+      isEnterpriseInstall,
     };
 
-    const result = await this.installerService.installer.authorize(query);
+    let result: AuthorizeResult;
+    try {
+      result = await this.installerService.installer.authorize(query);
 
-    if (result.botToken == null) {
-      res.writeHead(403);
+      if (result.botToken == null) {
+        res.writeHead(403, `The installation for the team(${teamId || enterpriseId}) has no botToken`);
+        return res.end();
+      }
+    }
+    catch (e) {
+      this.logger.error(e.message);
+
+      res.writeHead(500, e.message);
       return res.end();
     }
 
@@ -58,17 +77,30 @@ export class AuthorizeInteractionMiddleware implements IMiddleware {
   @Inject()
   installationRepository: InstallationRepository;
 
+  private logger: Logger;
+
+  constructor() {
+    this.logger = loggerFactory('slackbot-proxy:middlewares:AuthorizeInteractionMiddleware');
+  }
+
   async use(@Req() req: AuthedReq, @Res() res: Res): Promise<void> {
     const { body } = req;
 
+    if (body.payload == null) {
+      // do nothing
+      this.logger.info('body does not have payload');
+      return;
+    }
+
     const payload = JSON.parse(body.payload);
 
     // extract id from body
     const teamId = payload.team?.id;
-    const enterpriseId = body.enterprise?.id;
+    const enterpriseId = payload.enterprise?.id;
+    const isEnterpriseInstall = payload.is_enterprise_install === 'true';
 
     if (teamId == null && enterpriseId == null) {
-      res.writeHead(400);
+      res.writeHead(400, 'No installation found');
       return res.end();
     }
 
@@ -76,13 +108,22 @@ export class AuthorizeInteractionMiddleware implements IMiddleware {
     const query: InstallationQuery<boolean> = {
       teamId,
       enterpriseId,
-      isEnterpriseInstall: body.is_enterprise_install === 'true',
+      isEnterpriseInstall,
     };
 
-    const result = await this.installerService.installer.authorize(query);
+    let result: AuthorizeResult;
+    try {
+      result = await this.installerService.installer.authorize(query);
+
+      if (result.botToken == null) {
+        res.writeHead(403, `The installation for the team(${teamId || enterpriseId}) has no botToken`);
+        return res.end();
+      }
+    }
+    catch (e) {
+      this.logger.error(e.message);
 
-    if (result.botToken == null) {
-      res.writeHead(403);
+      res.writeHead(500, e.message);
       return res.end();
     }
 

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

@@ -5,6 +5,9 @@ import { AuthorizeResult } from '@slack/oauth';
 
 import { GrowiCommandProcessor } from '~/interfaces/growi-command-processor';
 
+
+const isProduction = process.env.NODE_ENV === 'production';
+
 @Service()
 export class RegisterService implements GrowiCommandProcessor {
 
@@ -13,7 +16,7 @@ export class RegisterService implements GrowiCommandProcessor {
     const { botToken } = authorizeResult;
 
     // tmp use process.env
-    const client = new WebClient(botToken, { logLevel: LogLevel.DEBUG });
+    const client = new WebClient(botToken, { logLevel: isProduction ? LogLevel.DEBUG : LogLevel.INFO });
     await client.views.open({
       trigger_id: body.trigger_id,
       view: {
@@ -33,7 +36,7 @@ export class RegisterService implements GrowiCommandProcessor {
         blocks: [
           generateInputSectionBlock('growiDomain', 'GROWI domain', 'contents_input', false, 'https://example.com'),
           generateInputSectionBlock('growiAccessToken', 'GROWI ACCESS_TOKEN', 'contents_input', false, 'jBMZvpk.....'),
-          generateInputSectionBlock('proxyToken', 'PROXY ACCESS_TOKEM', 'contents_input', false, 'jBMZvpk.....'),
+          generateInputSectionBlock('proxyToken', 'PROXY ACCESS_TOKEN', 'contents_input', false, 'jBMZvpk.....'),
         ],
       },
     });

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

@@ -275,6 +275,7 @@
     },
     "bot_reset_successful": "Bot settings have been reset.",
     "copied_to_clipboard": "Copied to clipboard",
+    "set_scope": "Please set up Bot Token Scopes from Slack settings",
     "modal": {
       "warning": "Warning",
       "sure_change_bot_type": "Are you sure you want to change the bot type?",
@@ -287,13 +288,16 @@
       "discard": "Discard",
       "generate": "Generate"
     },
-    "custom_bot_without_proxy_settings": "Custom Bot (Without-Proxy) Settings",
-    "without_proxy": {
+    "official_bot_settings": "Official bot Settings",
+    "custom_bot_without_proxy_settings": "Custom Bot without proxy Settings",
+    "accordion": {
       "create_bot": "Create Bot",
       "how_to_create_a_bot": "How to create a bot",
       "install_bot_to_slack": "Install Bot To Slack",
       "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",
+      "register_proxy_url": "Register Proxy URL with GROWI",
       "click_allow": "Select \"Allow\".",
       "install_complete_if_checked": "Confirm that \"Install your app\" is checked.",
       "invite_bot_to_channel": "Invite GROWI bot to channel by calling @example.",

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

@@ -273,6 +273,7 @@
     },
     "bot_reset_successful": "Botの設定を消去しました。",
     "copied_to_clipboard": "クリップボードにコピーされました。",
+    "set_scope": "Slackの設定画面からBot Token Scopeを設定してください",
     "modal": {
       "warning": "注意",
       "sure_change_bot_type": "Botの種類を変更しますか?",
@@ -286,12 +287,14 @@
       "generate": "発行"
     },
     "custom_bot_without_proxy_settings": "Custom Bot (Without-Proxy) 設定",
-    "without_proxy": {
+    "accordion": {
       "create_bot": "Bot を作成する",
       "how_to_create_a_bot": "作成方法はこちら",
-      "install_bot_to_slack": "Bot を Slackにインストールする",
+      "install_bot_to_slack": "Bot を Slack にインストールする",
       "select_install_your_app": "Install your app をクリックします。",
       "select_install_to_workspace": "Install to Workspace をクリックします。",
+      "register_official_bot_proxy_service": "アクセストークンの発行 / GROWI Official Bot Proxy サービスへの登録",
+      "register_proxy_url": "Proxy の URLをGROWIに登録する",
       "click_allow": "遷移先の画面にて、Allowをクリックします。",
       "install_complete_if_checked": "Install your app の右側に緑色のチェックがつけばワークスペースへのインストール完了です。",
       "invite_bot_to_channel": "GROWI bot を使いたいチャンネルに @example を使用して招待します。",

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

@@ -283,6 +283,7 @@
     },
     "bot_reset_successful": "删除了BOT设置。",
     "copied_to_clipboard": "它已复制到剪贴板。",
+    "set_scope": "在Slack设置页面中配置Bot Token Scope。",
     "modal": {
       "warning": "警告",
       "sure_change_bot_type": "您确定要更改设置吗?",
@@ -296,12 +297,14 @@
       "generate": "生成"
     },
     "custom_bot_without_proxy_settings": "Custom Bot (Without-Proxy) 设置",
-    "without_proxy": {
+    "accordion": {
       "create_bot": "创建 Bot",
-      "how_to_create_a_bot": "如何创建一个BOT",
-      "install_bot_to_slack": "将Bot安装到Slack",
+      "how_to_create_a_bot": "如何创建一个 Bot",
+      "install_bot_to_slack": "将 Bot 安装到 Slack",
       "select_install_your_app": "选择 \"Install your app\"。",
       "select_install_to_workspace": "选择 \"Install to Workspace\"。",
+      "register_official_bot_proxy_service": "发行访问令牌 / 注册 GROWI 官方 Bot 代理服务",
+      "register_proxy_url": "向 GROWI 注册代理 URL",
       "click_allow": "选择 \"Allow\"。",
       "install_complete_if_checked": "确认已选中 \"Install your app\"。",
       "invite_bot_to_channel": "通过调用 @example 邀请 GROWI Bot 进行频道。",

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

@@ -5,6 +5,7 @@ import AppContainer from '../../../services/AppContainer';
 import AdminAppContainer from '../../../services/AdminAppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import CustomBotWithProxyIntegrationCard from './CustomBotWithProxyIntegrationCard';
+import CustomBotWithProxySettingsAccordion from './CustomBotWithProxySettingsAccordion';
 
 const CustomBotWithProxySettings = (props) => {
   // eslint-disable-next-line no-unused-vars
@@ -19,6 +20,9 @@ const CustomBotWithProxySettings = (props) => {
 
       <CustomBotWithProxyIntegrationCard />
 
+      <div className="my-5 mx-3">
+        <CustomBotWithProxySettingsAccordion />
+      </div>
     </>
   );
 };

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

@@ -0,0 +1,32 @@
+import React from 'react';
+import Accordion from '../Common/Accordion';
+
+const CustomBotWithProxySettingsAccordion = () => {
+
+  return (
+    <div className="card border-0 rounded-lg shadow overflow-hidden">
+      <Accordion
+        title={<><span className="mr-2">①</span>First Accordion</>}
+      >
+        1
+      </Accordion>
+      <Accordion
+        title={<><span className="mr-2">②</span>Second Accordion</>}
+      >
+        2
+      </Accordion>
+      <Accordion
+        title={<><span className="mr-2">③</span>Third Accordion</>}
+      >
+        3
+      </Accordion>
+      <Accordion
+        title={<><span className="mr-2">④</span>Fourth Accordion</>}
+      >
+        4
+      </Accordion>
+    </div>
+  );
+};
+
+export default CustomBotWithProxySettingsAccordion;

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

@@ -14,7 +14,7 @@ const CustomBotWithoutProxyIntegrationCard = (props) => {
           {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>
+              <h5 className="font-weight-bold">{props.slackWSNameInWithoutProxy}</h5>
               <img width={20} height={20} src="/images/slack-integration/growi-bot-kun-icon.png" />
             </div>
           </div>
@@ -23,13 +23,16 @@ const CustomBotWithoutProxyIntegrationCard = (props) => {
       </div>
 
       <div className="text-center w-25">
-        {props.isSetupSlackBot && (
+        {props.isSlackScopeSet && (
         <div className="mt-5">
-          <p className="text-success"><small className="fa fa-check"> {t('admin:slack_integration.integration_sentence.integration_successful')}</small></p>
+          <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.isSetupSlackBot && (
+        {!props.isSlackScopeSet && (
         <div className="mt-4">
           <small
             className="text-secondary m-0"
@@ -54,7 +57,7 @@ const CustomBotWithoutProxyIntegrationCard = (props) => {
 CustomBotWithoutProxyIntegrationCard.propTypes = {
   siteName: PropTypes.string.isRequired,
   slackWSNameInWithoutProxy: PropTypes.string,
-  isSetupSlackBot: PropTypes.bool.isRequired,
+  isSlackScopeSet: PropTypes.bool.isRequired,
 };
 
 export default CustomBotWithoutProxyIntegrationCard;

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

@@ -43,7 +43,7 @@ const CustomBotWithoutProxySettings = (props) => {
       <CustomBotWithoutProxyIntegrationCard
         siteName={siteName}
         slackWSNameInWithoutProxy={props.slackWSNameInWithoutProxy}
-        isSetupSlackBot={props.isSetupSlackBot}
+        isSlackScopeSet={props.isSlackScopeSet}
       />
 
       <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_without_proxy_settings')}</h2>
@@ -82,7 +82,7 @@ CustomBotWithoutProxySettings.propTypes = {
   slackBotTokenEnv: PropTypes.string,
   isRgisterSlackCredentials: PropTypes.bool,
   isConnectedToSlack: PropTypes.bool,
-  isSetupSlackBot: PropTypes.bool,
+  isSlackScopeSet: PropTypes.bool,
   slackWSNameInWithoutProxy: PropTypes.string,
   fetchSlackIntegrationData: PropTypes.func,
 };

+ 14 - 15
src/client/js/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -102,13 +102,13 @@ const CustomBotWithoutProxySettingsAccordion = ({
     <div className="card border-0 rounded-lg shadow overflow-hidden">
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CREATE_BOT)}
-        title={<><span className="mr-2">①</span>{t('admin:slack_integration.without_proxy.create_bot')}</>}
+        title={<><span className="mr-2">①</span>{t('admin:slack_integration.accordion.create_bot')}</>}
       >
         <div className="row my-5">
           <div className="mx-auto">
             <div>
               <button type="button" className="btn btn-primary text-nowrap mx-1" onClick={() => window.open('https://api.slack.com/apps', '_blank')}>
-                {t('admin:slack_integration.without_proxy.create_bot')}
+                {t('admin:slack_integration.accordion.create_bot')}
                 <i className="fa fa-external-link ml-2" aria-hidden="true" />
               </button>
             </div>
@@ -116,7 +116,7 @@ const CustomBotWithoutProxySettingsAccordion = ({
             <a href="#">
               <p className="text-center mt-1">
                 <small>
-                  {t('admin:slack_integration.without_proxy.how_to_create_a_bot')}
+                  {t('admin:slack_integration.accordion.how_to_create_a_bot')}
                   <i className="fa fa-external-link ml-2" aria-hidden="true" />
                 </small>
               </p>
@@ -126,18 +126,18 @@ const CustomBotWithoutProxySettingsAccordion = ({
       </Accordion>
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.INSTALL_BOT)}
-        title={<><span className="mr-2">②</span>{t('admin:slack_integration.without_proxy.install_bot_to_slack')}</>}
+        title={<><span className="mr-2">②</span>{t('admin:slack_integration.accordion.install_bot_to_slack')}</>}
       >
         <div className="container w-75 py-5">
-          <p>1. {t('admin:slack_integration.without_proxy.select_install_your_app')}</p>
+          <p>1. {t('admin:slack_integration.accordion.select_install_your_app')}</p>
           <img src="/images/slack-integration/slack-bot-install-your-app-introduction.png" className="border border-light img-fluid mb-5" />
-          <p>2. {t('admin:slack_integration.without_proxy.select_install_to_workspace')}</p>
+          <p>2. {t('admin:slack_integration.accordion.select_install_to_workspace')}</p>
           <img src="/images/slack-integration/slack-bot-install-to-workspace.png" className="border border-light img-fluid mb-5" />
-          <p>3. {t('admin:slack_integration.without_proxy.click_allow')}</p>
+          <p>3. {t('admin:slack_integration.accordion.click_allow')}</p>
           <img src="/images/slack-integration/slack-bot-install-your-app-transition-destination.png" className="border border-light img-fluid mb-5" />
-          <p>4. {t('admin:slack_integration.without_proxy.install_complete_if_checked')}</p>
+          <p>4. {t('admin:slack_integration.accordion.install_complete_if_checked')}</p>
           <img src="/images/slack-integration/slack-bot-install-your-app-complete.png" className="border border-light img-fluid mb-5" />
-          <p>5. {t('admin:slack_integration.without_proxy.invite_bot_to_channel')}</p>
+          <p>5. {t('admin:slack_integration.accordion.invite_bot_to_channel')}</p>
           <img src="/images/slack-integration/slack-bot-install-to-workspace-joined-bot.png" className="border border-light img-fluid mb-1" />
           <img src="/images/slack-integration/slack-bot-install-your-app-introduction-to-channel.png" className="border border-light img-fluid" />
         </div>
@@ -145,7 +145,7 @@ const CustomBotWithoutProxySettingsAccordion = ({
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.REGISTER_SLACK_CONFIGURATION)}
         // eslint-disable-next-line max-len
-        title={<><span className="mr-2">③</span>{t('admin:slack_integration.without_proxy.register_secret_and_token')}{isRegisterSlackCredentials && <i className="ml-3 text-success fa fa-check"></i>}</>}
+        title={<><span className="mr-2">③</span>{t('admin:slack_integration.accordion.register_secret_and_token')}{isRegisterSlackCredentials && <i className="ml-3 text-success fa fa-check"></i>}</>}
       >
         <CustomBotWithoutProxySecretTokenSection
           updateSecretTokenHandler={updateSecretTokenHandler}
@@ -160,9 +160,9 @@ const CustomBotWithoutProxySettingsAccordion = ({
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CONNECTION_TEST)}
         // eslint-disable-next-line max-len
-        title={<><span className="mr-2">④</span>{t('admin:slack_integration.without_proxy.test_connection')}{isSendTestMessage && <i className="ml-3 text-success fa fa-check"></i>}</>}
+        title={<><span className="mr-2">④</span>{t('admin:slack_integration.accordion.test_connection')}{isSendTestMessage && <i className="ml-3 text-success fa fa-check"></i>}</>}
       >
-        <p className="text-center m-4">{t('admin:slack_integration.without_proxy.test_connection_by_pressing_button')}</p>
+        <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" onSubmit={e => submitForm(e)}>
             <div className="col-8 input-group-prepend">
@@ -186,9 +186,9 @@ const CustomBotWithoutProxySettingsAccordion = ({
           </form>
         </div>
         {connectionErrorMessage != null
-        && <p className="text-danger text-center my-4">{t('admin:slack_integration.without_proxy.error_check_logs_below')}</p>}
+        && <p className="text-danger text-center my-4">{t('admin:slack_integration.accordion.error_check_logs_below')}</p>}
         {connectionSuccessMessage != null
-         && <p className="text-info text-center my-4">{t('admin:slack_integration.without_proxy.send_message_to_slack_work_space')}</p>}
+        && <p className="text-info text-center my-4">{t('admin:slack_integration.accordion.send_message_to_slack_work_space')}</p>}
         <form>
           <div className="row my-3 justify-content-center">
             <div className="form-group slack-connection-log w-25">
@@ -226,7 +226,6 @@ CustomBotWithoutProxySettingsAccordion.propTypes = {
 
   adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
   activeStep: PropTypes.oneOf(Object.values(botInstallationStep)).isRequired,
-  isSetupSlackBot: PropTypes.bool,
 };
 
 export default CustomBotWithoutProxySettingsAccordionWrapper;

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

@@ -1,11 +1,19 @@
 import React from 'react';
+import { useTranslation } from 'react-i18next';
+import OfficialBotSettingsAccordion from './OfficialbotSettingsAccordion';
 
 const OfficialBotSettings = () => {
+  const { t } = useTranslation();
 
   return (
-    <div className="row my-5">
-      <h1>Official Bot Settings Component</h1>
-    </div>
+    <>
+      <h2 className="admin-setting-header">{t('admin:slack_integration.official_bot_settings')}</h2>
+
+      <div className="my-5 mx-3">
+        <OfficialBotSettingsAccordion />
+      </div>
+    </>
+
   );
 };
 

+ 38 - 0
src/client/js/components/Admin/SlackIntegration/OfficialbotSettingsAccordion.jsx

@@ -0,0 +1,38 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import Accordion from '../Common/Accordion';
+
+const OfficialBotSettingsAccordion = () => {
+  const { t } = useTranslation();
+
+  return (
+    <div className="card border-0 rounded-lg shadow overflow-hidden">
+      <Accordion
+        title={<><span className="mr-2">①</span>{t('admin:slack_integration.accordion.install_bot_to_slack')}</>}
+      >
+        {/* TODO: GW-5824 add accordion contents  */}
+        hoge
+      </Accordion>
+      <Accordion
+        title={<><span className="mr-2">②</span>{t('admin:slack_integration.accordion.register_official_bot_proxy_service')}</>}
+      >
+        {/* TODO: GW-5824 add accordion contents  */}
+        hoge
+      </Accordion>
+      <Accordion
+        title={<><span className="mr-2">③</span>{t('admin:slack_integration.accordion.register_proxy_url')}</>}
+      >
+        {/* TODO: GW-5824 add accordion contents  */}
+        hoge
+      </Accordion>
+      <Accordion
+        title={<><span className="mr-2">④</span>{t('admin:slack_integration.accordion.test_connection')}</>}
+      >
+        {/* TODO: GW-5824 add accordion contents  */}
+        hoge
+      </Accordion>
+    </div>
+  );
+};
+
+export default OfficialBotSettingsAccordion;

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

@@ -25,8 +25,8 @@ const SlackIntegration = (props) => {
   const [isConnectedToSlack, setIsConnectedToSlack] = useState(false);
   const [isRegisterSlackCredentials, setIsRegisterSlackCredentials] = useState(false);
   const [isSendTestMessage, setIsSendTestMessage] = useState(false);
-  const [isSetupSlackBot, setIsSetupSlackBot] = useState(false);
   const [slackWSNameInWithoutProxy, setSlackWSNameInWithoutProxy] = useState(null);
+  const [isSlackScopeSet, setIsSlackScopeSet] = useState(false);
 
   const fetchSlackWorkSpaceNameInWithoutProxy = useCallback(async() => {
     if (!isConnectedToSlack) {
@@ -35,11 +35,19 @@ const SlackIntegration = (props) => {
     try {
       const res = await appContainer.apiv3.get('/slack-integration/custom-bot-without-proxy/slack-workspace-name');
       setSlackWSNameInWithoutProxy(res.data.slackWorkSpaceName);
+      setIsSlackScopeSet(true);
     }
     catch (err) {
-      toastError(err);
+      if (err[0].message === 'missing_scope') {
+        setSlackWSNameInWithoutProxy(null);
+        setIsSlackScopeSet(false);
+        toastError(err, t('admin:slack_integration.set_scope'));
+      }
+      else {
+        toastError(err);
+      }
     }
-  }, [appContainer.apiv3, isConnectedToSlack]);
+  }, [appContainer.apiv3, isConnectedToSlack, t]);
 
   const fetchSlackIntegrationData = useCallback(async() => {
     try {
@@ -47,7 +55,7 @@ const SlackIntegration = (props) => {
       const { currentBotType, customBotWithoutProxySettings } = response.data.slackBotSettingParams;
       const {
         slackSigningSecret, slackBotToken, slackSigningSecretEnvVars, slackBotTokenEnvVars,
-        isSetupSlackBot, isConnectedToSlack,
+        isConnectedToSlack,
       } = customBotWithoutProxySettings;
 
       setCurrentBotType(currentBotType);
@@ -55,7 +63,6 @@ const SlackIntegration = (props) => {
       setSlackBotToken(slackBotToken);
       setSlackSigningSecretEnv(slackSigningSecretEnvVars);
       setSlackBotTokenEnv(slackBotTokenEnvVars);
-      setIsSetupSlackBot(isSetupSlackBot);
       setIsConnectedToSlack(isConnectedToSlack);
 
       fetchSlackWorkSpaceNameInWithoutProxy();
@@ -111,6 +118,7 @@ const SlackIntegration = (props) => {
       setIsConnectedToSlack(false);
       setIsSendTestMessage(false);
       setSlackWSNameInWithoutProxy(null);
+      setIsSlackScopeSet(false);
     }
     catch (err) {
       toastError(err);
@@ -129,7 +137,7 @@ const SlackIntegration = (props) => {
           isSendTestMessage={isSendTestMessage}
           isRegisterSlackCredentials={isRegisterSlackCredentials}
           isConnectedToSlack={isConnectedToSlack}
-          isSetupSlackBot={isSetupSlackBot}
+          isSlackScopeSet={isSlackScopeSet}
           slackBotTokenEnv={slackBotTokenEnv}
           slackBotToken={slackBotToken}
           slackSigningSecretEnv={slackSigningSecretEnv}

+ 4 - 2
src/server/routes/apiv3/slack-integration.js

@@ -105,7 +105,6 @@ module.exports = (crowi) => {
         slackBotTokenEnvVars: crowi.configManager.getConfigFromEnvVars('crowi', 'slackbot:token'),
         slackSigningSecret: crowi.configManager.getConfig('crowi', 'slackbot:signingSecret'),
         slackBotToken: crowi.configManager.getConfig('crowi', 'slackbot:token'),
-        isSetupSlackBot: crowi.slackBotService.isSetupSlackBot,
         isConnectedToSlack: crowi.slackBotService.isConnectedToSlack,
       },
       // TODO imple when creating with proxy
@@ -232,7 +231,10 @@ module.exports = (crowi) => {
       return res.apiv3({ slackWorkSpaceName });
     }
     catch (error) {
-      const msg = 'Error occured in slack_bot_token';
+      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);
     }

+ 0 - 3
src/server/service/slackbot.js

@@ -19,7 +19,6 @@ class SlackBotService extends S2sMessageHandlable {
     this.client = null;
     this.searchService = null;
 
-    this.isSetupSlackBot = false;
     this.isConnectedToSlack = false;
 
     this.lastLoadedAt = null;
@@ -28,14 +27,12 @@ class SlackBotService extends S2sMessageHandlable {
   }
 
   async initialize() {
-    this.isSetupSlackBot = false;
     this.isConnectedToSlack = false;
     const token = this.crowi.configManager.getConfig('crowi', 'slackbot:token');
 
     if (token != null) {
       this.client = new WebClient(token, { logLevel: LogLevel.DEBUG });
       logger.debug('SlackBot: setup is done');
-      this.isSetupSlackBot = true;
       await this.sendAuthTest();
     }