Procházet zdrojové kódy

Merge remote-tracking branch 'origin/feat/growi-bot' into fix/input-form-of-secret-and-token

Yuki Takei před 4 roky
rodič
revize
bdaf12b207

+ 33 - 28
packages/slack/src/utils/check-communicable.ts

@@ -67,32 +67,47 @@ const retrieveWorkspaceName = async(client: WebClient): Promise<string> => {
   return (result as any).team?.name;
   return (result as any).team?.name;
 };
 };
 
 
+/**
+ * @param token bot OAuth token
+ * @returns
+ */
+export const getConnectionStatus = async(token:string): Promise<ConnectionStatus> => {
+  const client = generateWebClient(token);
+  const status: ConnectionStatus = {};
+
+  try {
+    // try to connect
+    const resultTestSlackApiServer = await testSlackApiServer(client);
+    // check scope
+    await checkSlackScopes(resultTestSlackApiServer);
+    // retrieve workspace name
+    status.workspaceName = await retrieveWorkspaceName(client);
+  }
+  catch (err) {
+    status.error = err;
+  }
+
+  return status;
+};
+
 /**
 /**
  * Get token string to ConnectionStatus map
  * Get token string to ConnectionStatus map
- * @param tokens Array of bot OAuth token
+ * @param keys Array of bot OAuth token or specific key
+ * @param botTokenResolver function to convert from key to token
  * @returns
  * @returns
  */
  */
-export const getConnectionStatuses = async(tokens: string[]): Promise<{[key: string]: ConnectionStatus}> => {
-  const map = tokens
+export const getConnectionStatuses = async(keys: string[], botTokenResolver?: (key: string) => string): Promise<{[key: string]: ConnectionStatus}> => {
+  const map = keys
     .reduce<Promise<Map<string, ConnectionStatus>>>(
     .reduce<Promise<Map<string, ConnectionStatus>>>(
-      async(acc, token) => {
-        const client = generateWebClient(token);
-
-        const status: ConnectionStatus = {};
-        try {
-          // try to connect
-          await testSlackApiServer(client);
-          // retrieve workspace name
-          status.workspaceName = await retrieveWorkspaceName(client);
-        }
-        catch (err) {
-          status.error = err;
+      async(acc, key) => {
+        let token = key;
+        if (botTokenResolver != null) {
+          token = botTokenResolver(key);
         }
         }
+        const status: ConnectionStatus = await getConnectionStatus(token);
 
 
-        (await acc).set(token, status);
-
+        (await acc).set(key, status);
         return acc;
         return acc;
-
       },
       },
       // define initial accumulator
       // define initial accumulator
       Promise.resolve(new Map<string, ConnectionStatus>()),
       Promise.resolve(new Map<string, ConnectionStatus>()),
@@ -102,16 +117,6 @@ export const getConnectionStatuses = async(tokens: string[]): Promise<{[key: str
   return Object.fromEntries(await map);
   return Object.fromEntries(await map);
 };
 };
 
 
-/**
- * @param token bot OAuth token
- * @returns
- */
-export const testToSlack = async(token:string): Promise<void> => {
-  const client = generateWebClient(token);
-  const res = await testSlackApiServer(client);
-  await checkSlackScopes(res);
-};
-
 export const sendSuccessMessage = async(token:string, channel:string, appSiteUrl:string): Promise<void> => {
 export const sendSuccessMessage = async(token:string, channel:string, appSiteUrl:string): Promise<void> => {
   const client = generateWebClient(token);
   const client = generateWebClient(token);
   await client.chat.postMessage({
   await client.chat.postMessage({

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

@@ -6,7 +6,7 @@ import axios from 'axios';
 import { WebAPICallOptions, WebAPICallResult } from '@slack/web-api';
 import { WebAPICallOptions, WebAPICallResult } from '@slack/web-api';
 
 
 import {
 import {
-  verifyGrowiToSlackRequest, getConnectionStatuses, testToSlack, generateWebClient,
+  verifyGrowiToSlackRequest, getConnectionStatuses, getConnectionStatus, generateWebClient,
 } from '@growi/slack';
 } from '@growi/slack';
 
 
 import { GrowiReq } from '~/interfaces/growi-to-slack/growi-req';
 import { GrowiReq } from '~/interfaces/growi-to-slack/growi-req';
@@ -63,13 +63,17 @@ export class GrowiToSlackCtrl {
 
 
     logger.debug(`${relations.length} relations found`, relations);
     logger.debug(`${relations.length} relations found`, relations);
 
 
-    // extract bot token
-    const tokens: string[] = relations
-      .map(relation => relation.installation?.data?.bot?.token)
-      .filter((v): v is string => v != null); // filter out null values
+    // key: tokenGtoP, value: botToken
+    const botTokenResolverMapping: {[tokenGtoP:string]:string} = {};
 
 
-    const connectionStatuses = await getConnectionStatuses(tokens);
+    relations.forEach((relation) => {
+      const botToken = relation.installation?.data?.bot?.token;
+      if (botToken != null) {
+        botTokenResolverMapping[relation.tokenGtoP] = botToken;
+      }
+    });
 
 
+    const connectionStatuses = await getConnectionStatuses(Object.keys(botTokenResolverMapping), (tokenGtoP:string) => botTokenResolverMapping[tokenGtoP]);
     return res.send({ connectionStatuses });
     return res.send({ connectionStatuses });
   }
   }
 
 
@@ -107,12 +111,9 @@ export class GrowiToSlackCtrl {
         return res.status(400).send({ message: `failed to request to GROWI. err: ${err.message}` });
         return res.status(400).send({ message: `failed to request to GROWI. err: ${err.message}` });
       }
       }
 
 
-      try {
-        await testToSlack(token);
-      }
-      catch (err) {
-        logger.error(err);
-        return res.status(400).send({ message: `failed to test. err: ${err.message}` });
+      const status = await getConnectionStatus(token);
+      if (status.error != null) {
+        return res.status(400).send({ message: `failed to get connection. err: ${status.error}` });
       }
       }
 
 
       return res.send({ relation, slackBotToken: token });
       return res.send({ relation, slackBotToken: token });
@@ -145,12 +146,9 @@ export class GrowiToSlackCtrl {
       return res.status(400).send({ message: 'installation is invalid' });
       return res.status(400).send({ message: 'installation is invalid' });
     }
     }
 
 
-    try {
-      await testToSlack(token);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.status(400).send({ message: `failed to test. err: ${err.message}` });
+    const status = await getConnectionStatus(token);
+    if (status.error != null) {
+      return res.status(400).send({ message: `failed to get connection. err: ${status.error}` });
     }
     }
 
 
     logger.debug('relation test is success', order);
     logger.debug('relation test is success', order);

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

@@ -60,8 +60,7 @@ const BotTypeCard = (props) => {
             </span>
             </span>
           )}
           )}
 
 
-          {/* TODO: add an appropriate links by GW-5614 */}
-          <i className={`fa fa-external-link btn-link ${props.isActive ? 'grw-botcard-title-active' : ''}`} aria-hidden="true"></i>
+          <i className={props.isActive ? 'grw-botcard-title-active' : ''} aria-hidden="true"></i>
         </h3>
         </h3>
       </div>
       </div>
       <div className="card-body p-4">
       <div className="card-body p-4">

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

@@ -4,8 +4,11 @@ import PropTypes from 'prop-types';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
 const ProxyCircle = () => (
 const ProxyCircle = () => (
-  <div className="grw-bridge-proxy-circle position-absolute bg-primary border-light">
-    <p className="circle-inner text-light font-weight-bold">Proxy Server</p>
+  <div className="grw-bridge-proxy-circle">
+    <div className="circle position-absolute bg-primary border-light rounded-circle">
+      <p className="circle-inner text-light font-weight-bold d-none d-lg-inline">Proxy Server</p>
+      <p className="circle-inner mt-5 d-block d-lg-none">ProxyServer</p>
+    </div>
   </div>
   </div>
 );
 );
 
 

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

@@ -71,7 +71,10 @@ const CustomBotWithProxySettings = (props) => {
 
 
   return (
   return (
     <>
     <>
-      <h2 className="admin-setting-header mb-2">{t('admin:slack_integration.custom_bot_with_proxy_integration')}</h2>
+      <h2 className="admin-setting-header mb-2">{t('admin:slack_integration.custom_bot_with_proxy_integration')}
+        {/* TODO: add an appropriate links by GW-5614 */}
+        <i className="fa fa-external-link btn-link ml-2" aria-hidden="true"></i>
+      </h2>
 
 
       {slackAppIntegrations.length !== 0 && (
       {slackAppIntegrations.length !== 0 && (
         <>
         <>
@@ -102,12 +105,16 @@ const CustomBotWithProxySettings = (props) => {
 
 
       <div className="mx-3">
       <div className="mx-3">
         {slackAppIntegrations.map((slackAppIntegration, i) => {
         {slackAppIntegrations.map((slackAppIntegration, i) => {
-          const { tokenGtoP, tokenPtoG } = slackAppIntegration;
+          const { tokenGtoP, tokenPtoG, _id } = slackAppIntegration;
+          const workspaceName = connectionStatuses[_id]?.workspaceName;
           return (
           return (
             <React.Fragment key={slackAppIntegration._id}>
             <React.Fragment key={slackAppIntegration._id}>
-              <div className="d-flex justify-content-end">
+              <div className="my-3 d-flex align-items-center justify-content-between">
+                <h2 id={_id || `settings-accordions-${i}`}>
+                  {(workspaceName != null) ? `${workspaceName} Work Space` : `Settings #${i}`}
+                </h2>
                 <button
                 <button
-                  className="my-3 btn btn-outline-danger"
+                  className="btn btn-outline-danger"
                   type="button"
                   type="button"
                   onClick={() => setIntegrationIdToDelete(slackAppIntegration._id)}
                   onClick={() => setIntegrationIdToDelete(slackAppIntegration._id)}
                 >
                 >

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

@@ -6,35 +6,24 @@ import AdminAppContainer from '../../../services/AdminAppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import CustomBotWithoutProxySettingsAccordion, { botInstallationStep } from './CustomBotWithoutProxySettingsAccordion';
 import CustomBotWithoutProxySettingsAccordion, { botInstallationStep } from './CustomBotWithoutProxySettingsAccordion';
 import CustomBotWithoutProxyConnectionStatus from './CustomBotWithoutProxyConnectionStatus';
 import CustomBotWithoutProxyConnectionStatus from './CustomBotWithoutProxyConnectionStatus';
-import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 
 
 const CustomBotWithoutProxySettings = (props) => {
 const CustomBotWithoutProxySettings = (props) => {
-  const { appContainer, onResetSettings, connectionStatuses } = props;
+  const { appContainer, connectionStatuses } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const [siteName, setSiteName] = useState('');
   const [siteName, setSiteName] = useState('');
-  const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState(false);
   const [isIntegrationSuccess, setIsIntegrationSuccess] = useState(false);
   const [isIntegrationSuccess, setIsIntegrationSuccess] = useState(false);
-  const [connectionMessage, setConnectionMessage] = useState('');
-  const [connectionErrorCode, setConnectionErrorCode] = useState(null);
+  const [connectionMessage, setConnectionMessage] = useState(null);
   const [testChannel, setTestChannel] = useState('');
   const [testChannel, setTestChannel] = useState('');
 
 
-  const resetSettings = async() => {
-    if (onResetSettings == null) {
-      return;
-    }
-    onResetSettings();
-  };
-
   const testConnection = async() => {
   const testConnection = async() => {
     try {
     try {
       await appContainer.apiv3.post('/slack-integration-settings/without-proxy/test', { channel: testChannel });
       await appContainer.apiv3.post('/slack-integration-settings/without-proxy/test', { channel: testChannel });
-      setConnectionMessage('Send the message to slack work space.');
+      setConnectionMessage('');
       setIsIntegrationSuccess(true);
       setIsIntegrationSuccess(true);
     }
     }
     catch (err) {
     catch (err) {
-      setConnectionErrorCode(err[0].code);
-      setConnectionMessage(err[0].message);
+      setConnectionMessage(err[0]);
       setIsIntegrationSuccess(false);
       setIsIntegrationSuccess(false);
     }
     }
   };
   };
@@ -48,9 +37,15 @@ const CustomBotWithoutProxySettings = (props) => {
     setSiteName(siteName);
     setSiteName(siteName);
   }, [appContainer]);
   }, [appContainer]);
 
 
+  const workspaceName = connectionStatuses[props.slackBotToken]?.workspaceName;
+
   return (
   return (
+
     <>
     <>
-      <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_without_proxy_integration')}</h2>
+      <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_without_proxy_integration')}
+        {/* TODO: add an appropriate links by GW-5614 */}
+        <i className="fa fa-external-link btn-link ml-2" aria-hidden="true"></i>
+      </h2>
 
 
       <CustomBotWithoutProxyConnectionStatus
       <CustomBotWithoutProxyConnectionStatus
         siteName={siteName}
         siteName={siteName}
@@ -59,33 +54,22 @@ const CustomBotWithoutProxySettings = (props) => {
 
 
       <h2 className="admin-setting-header">{t('admin:slack_integration.integration_procedure')}</h2>
       <h2 className="admin-setting-header">{t('admin:slack_integration.integration_procedure')}</h2>
 
 
-      {(props.slackSigningSecret || props.slackBotToken) && (
-      <button
-        className="mx-3 pull-right btn text-danger border-danger"
-        type="button"
-        onClick={() => setIsDeleteConfirmModalShown(true)}
-      >{t('admin:slack_integration.reset')}
-      </button>
-      )}
-      <div className="my-5 mx-3">
-        {/* {isConnectedFailed && (<>Settings #1 <span className="text-danger">{t('admin:slack_integration.integration_failed')}</span></>)} */}
+      <div className="px-3">
+        <div className="my-3 d-flex align-items-center justify-content-between">
+          <h2 id={props.slackBotToken || 'settings-accordions'}>
+            {(workspaceName != null) ? `${workspaceName} Work Space` : 'Settings'}
+          </h2>
+        </div>
         <CustomBotWithoutProxySettingsAccordion
         <CustomBotWithoutProxySettingsAccordion
           {...props}
           {...props}
           activeStep={botInstallationStep.CREATE_BOT}
           activeStep={botInstallationStep.CREATE_BOT}
           connectionMessage={connectionMessage}
           connectionMessage={connectionMessage}
-          connectionErrorCode={connectionErrorCode}
           isIntegrationSuccess={isIntegrationSuccess}
           isIntegrationSuccess={isIntegrationSuccess}
           testChannel={testChannel}
           testChannel={testChannel}
           onTestFormSubmitted={testConnection}
           onTestFormSubmitted={testConnection}
           inputTestChannelHandler={inputTestChannelHandler}
           inputTestChannelHandler={inputTestChannelHandler}
         />
         />
       </div>
       </div>
-      <DeleteSlackBotSettingsModal
-        isResetAll={false}
-        isOpen={isDeleteConfirmModalShown}
-        onClose={() => setIsDeleteConfirmModalShown(false)}
-        onClickDeleteButton={resetSettings}
-      />
     </>
     </>
   );
   );
 };
 };
@@ -102,7 +86,6 @@ CustomBotWithoutProxySettings.propTypes = {
   slackBotTokenEnv: PropTypes.string,
   slackBotTokenEnv: PropTypes.string,
 
 
   isIntegrationSuccess: PropTypes.bool,
   isIntegrationSuccess: PropTypes.bool,
-  onResetSettings: PropTypes.func,
   connectionStatuses: PropTypes.object.isRequired,
   connectionStatuses: PropTypes.object.isRequired,
 };
 };
 
 

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

@@ -13,7 +13,7 @@ export const botInstallationStep = {
 
 
 const CustomBotWithoutProxySettingsAccordion = (props) => {
 const CustomBotWithoutProxySettingsAccordion = (props) => {
   const {
   const {
-    activeStep, connectionMessage, connectionErrorCode, testChannel,
+    activeStep, connectionMessage, testChannel,
     slackSigningSecret, slackBotToken, slackSigningSecretEnv, slackBotTokenEnv,
     slackSigningSecret, slackBotToken, slackSigningSecretEnv, slackBotTokenEnv,
     isIntegrationSuccess,
     isIntegrationSuccess,
     inputTestChannelHandler, onTestFormSubmitted,
     inputTestChannelHandler, onTestFormSubmitted,
@@ -33,13 +33,12 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
     onTestFormSubmitted();
     onTestFormSubmitted();
   };
   };
 
 
-
   let value = '';
   let value = '';
-  if (connectionMessage === 'Send the message to slack work space.' || connectionMessage === '') {
-    value = connectionMessage;
+  if (connectionMessage === '' || connectionMessage == null) {
+    value = '';
   }
   }
   else {
   else {
-    value = [connectionErrorCode, connectionMessage];
+    value = [connectionMessage.code, connectionMessage.message];
   }
   }
 
 
   let isEnterdSecretAndToken = false;
   let isEnterdSecretAndToken = false;
@@ -122,11 +121,11 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
             </button>
             </button>
           </form>
           </form>
         </div>
         </div>
-        {connectionMessage === ''
+        {connectionMessage == null
           ? <p></p>
           ? <p></p>
           : (
           : (
             <>
             <>
-              {connectionMessage === 'Send the message to slack work space.'
+              {connectionMessage === ''
                 ? <p className="text-info text-center my-4">{t('admin:slack_integration.accordion.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>
                 : <p className="text-danger text-center my-4">{t('admin:slack_integration.accordion.error_check_logs_below')}</p>
                 : <p className="text-danger text-center my-4">{t('admin:slack_integration.accordion.error_check_logs_below')}</p>
               }
               }
@@ -166,7 +165,6 @@ CustomBotWithoutProxySettingsAccordion.propTypes = {
   isIntegrationSuccess: PropTypes.bool,
   isIntegrationSuccess: PropTypes.bool,
   inputTestChannelHandler: PropTypes.func,
   inputTestChannelHandler: PropTypes.func,
   onTestFormSubmitted: PropTypes.func,
   onTestFormSubmitted: PropTypes.func,
-
 };
 };
 
 
 export default CustomBotWithoutProxySettingsAccordion;
 export default CustomBotWithoutProxySettingsAccordion;

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

@@ -77,7 +77,10 @@ const OfficialBotSettings = (props) => {
 
 
   return (
   return (
     <>
     <>
-      <h2 className="admin-setting-header">{t('admin:slack_integration.official_bot_integration')}</h2>
+      <h2 className="admin-setting-header">{t('admin:slack_integration.official_bot_integration')}
+        {/* TODO: add an appropriate links by GW-5614 */}
+        <i className="fa fa-external-link btn-link ml-2" aria-hidden="true"></i>
+      </h2>
       <CustomBotWithProxyConnectionStatus
       <CustomBotWithProxyConnectionStatus
         siteName={siteName}
         siteName={siteName}
         connectionStatuses={connectionStatuses}
         connectionStatuses={connectionStatuses}
@@ -102,13 +105,17 @@ const OfficialBotSettings = (props) => {
       <h2 className="admin-setting-header">{t('admin:slack_integration.integration_procedure')}</h2>
       <h2 className="admin-setting-header">{t('admin:slack_integration.integration_procedure')}</h2>
 
 
       <div className="mx-3">
       <div className="mx-3">
-        {slackAppIntegrations.map((slackAppIntegration) => {
-          const { tokenGtoP, tokenPtoG } = slackAppIntegration;
+        {slackAppIntegrations.map((slackAppIntegration, i) => {
+          const { tokenGtoP, tokenPtoG, _id } = slackAppIntegration;
+          const workspaceName = connectionStatuses[_id]?.workspaceName;
           return (
           return (
             <React.Fragment key={slackAppIntegration._id}>
             <React.Fragment key={slackAppIntegration._id}>
-              <div className="d-flex justify-content-end">
+              <div className="my-3 d-flex align-items-center justify-content-between">
+                <h2 id={_id || `settings-accordions-${i}`}>
+                  {(workspaceName != null) ? `${workspaceName} Work Space` : `Settings #${i}`}
+                </h2>
                 <button
                 <button
-                  className="my-3 btn btn-outline-danger"
+                  className="btn btn-outline-danger"
                   type="button"
                   type="button"
                   onClick={() => setIsDeleteConfirmModalShown(true)}
                   onClick={() => setIsDeleteConfirmModalShown(true)}
                 >
                 >

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

@@ -67,17 +67,6 @@ const SlackIntegration = (props) => {
     }
     }
   };
   };
 
 
-  const resetWithOutSettings = async() => {
-    try {
-      await appContainer.apiv3.put('/slack-integration-settings/bot-type', { currentBotType: 'customBotWithoutProxy' });
-      fetchSlackIntegrationData();
-      toastSuccess(t('admin:slack_integration.bot_reset_successful'));
-    }
-    catch (error) {
-      toastError(error);
-    }
-  };
-
   const createSlackIntegrationData = async() => {
   const createSlackIntegrationData = async() => {
     try {
     try {
       await appContainer.apiv3.put('/slack-integration-settings/slack-app-integrations');
       await appContainer.apiv3.put('/slack-integration-settings/slack-app-integrations');
@@ -156,7 +145,6 @@ const SlackIntegration = (props) => {
           slackSigningSecretEnv={slackSigningSecretEnv}
           slackSigningSecretEnv={slackSigningSecretEnv}
           slackSigningSecret={slackSigningSecret}
           slackSigningSecret={slackSigningSecret}
           slackWSNameInWithoutProxy={slackWSNameInWithoutProxy}
           slackWSNameInWithoutProxy={slackWSNameInWithoutProxy}
-          onResetSettings={resetWithOutSettings}
           fetchSlackIntegrationData={fetchSlackIntegrationData}
           fetchSlackIntegrationData={fetchSlackIntegrationData}
           onUpdatedSecretToken={changeSecretAndToken}
           onUpdatedSecretToken={changeSecretAndToken}
           connectionStatuses={connectionStatuses}
           connectionStatuses={connectionStatuses}

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

@@ -129,7 +129,7 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers
           { t('admin:slack_integration.access_token_settings.regenerate') }
           { t('admin:slack_integration.access_token_settings.regenerate') }
         </button>
         </button>
       </div>
       </div>
-      <p className="font-weight-bold">2. {t('admin:slack_integration.accordion.register_for_growi_official_bot_proxy_service')}</p>
+      <p className="font-weight-bold mt-5">2. {t('admin:slack_integration.accordion.register_for_growi_official_bot_proxy_service')}</p>
       <div className="d-flex flex-column align-items-center">
       <div className="d-flex flex-column align-items-center">
         <ol className="p-0">
         <ol className="p-0">
           <li>
           <li>

+ 20 - 9
src/client/styles/scss/_admin.scss

@@ -90,9 +90,6 @@ $slack-work-space-name-card-border: #efc1f6;
   Slack Integration
   Slack Integration
   */
   */
   .selecting-bot-type {
   .selecting-bot-type {
-    .btn-link {
-      font-size: 1rem;
-    }
     .supplementary-bot-name {
     .supplementary-bot-name {
       font-size: 1rem;
       font-size: 1rem;
     }
     }
@@ -118,6 +115,14 @@ $slack-work-space-name-card-border: #efc1f6;
     }
     }
   }
   }
 
 
+  .admin-slack-integration {
+    .admin-setting-header {
+      .btn-link {
+        font-size: 1rem;
+      }
+    }
+  }
+
   .bot-integration {
   .bot-integration {
     .admin-bot-card {
     .admin-bot-card {
       border-radius: 8px !important;
       border-radius: 8px !important;
@@ -131,12 +136,18 @@ $slack-work-space-name-card-border: #efc1f6;
     }
     }
 
 
     .grw-bridge-proxy-circle {
     .grw-bridge-proxy-circle {
-      left: 50%;
-      width: 100px;
-      height: 100px;
-      border: 13px solid;
-      border-radius: 50%;
-      transform: translate(-50%, -50%);
+      .circle {
+        left: 50%;
+        width: 100px;
+        height: 100px;
+        border: 13px solid;
+        transform: translate(-50%, -50%);
+        @include media-breakpoint-down(md) {
+          width: 50px;
+          height: 50px;
+          border: 8px solid;
+        }
+      }
 
 
       .circle-inner {
       .circle-inner {
         position: absolute;
         position: absolute;

+ 19 - 1
src/server/models/slack-app-integration.js

@@ -7,7 +7,7 @@ const schema = new mongoose.Schema({
 });
 });
 class SlackAppIntegration {
 class SlackAppIntegration {
 
 
-  static generateAccessToken() {
+  static generateAccessTokens() {
     const hasher1 = crypto.createHash('sha512');
     const hasher1 = crypto.createHash('sha512');
     const hasher2 = crypto.createHash('sha512');
     const hasher2 = crypto.createHash('sha512');
     const tokenGtoP = hasher1.update(new Date().getTime().toString() + process.env.SALT_FOR_GTOP_TOKEN);
     const tokenGtoP = hasher1.update(new Date().getTime().toString() + process.env.SALT_FOR_GTOP_TOKEN);
@@ -15,6 +15,24 @@ class SlackAppIntegration {
     return [tokenGtoP.digest('base64'), tokenPtoG.digest('base64')];
     return [tokenGtoP.digest('base64'), tokenPtoG.digest('base64')];
   }
   }
 
 
+  static async generateUniqueAccessTokens() {
+    let duplicateTokens;
+    let tokenGtoP;
+    let tokenPtoG;
+    let generateTokens;
+
+    do {
+      generateTokens = this.generateAccessTokens();
+      tokenGtoP = generateTokens[0];
+      tokenPtoG = generateTokens[1];
+      // eslint-disable-next-line no-await-in-loop
+      duplicateTokens = await this.findOne({ $or: [{ tokenGtoP }, { tokenPtoG }] });
+    } while (duplicateTokens != null);
+
+
+    return { tokenGtoP, tokenPtoG };
+  }
+
 }
 }
 
 
 module.exports = function(crowi) {
 module.exports = function(crowi) {

+ 22 - 31
src/server/routes/apiv3/slack-integration-settings.js

@@ -5,7 +5,7 @@ const axios = require('axios');
 const urljoin = require('url-join');
 const urljoin = require('url-join');
 const loggerFactory = require('@alias/logger');
 const loggerFactory = require('@alias/logger');
 
 
-const { getConnectionStatuses, testToSlack, sendSuccessMessage } = require('@growi/slack');
+const { getConnectionStatus, getConnectionStatuses, sendSuccessMessage } = require('@growi/slack');
 
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
@@ -149,7 +149,7 @@ module.exports = (crowi) => {
     }
     }
 
 
     // retrieve connection statuses
     // retrieve connection statuses
-    let connectionStatuses;
+    let connectionStatuses = {};
     if (currentBotType == null) {
     if (currentBotType == null) {
       // TODO imple null action
       // TODO imple null action
     }
     }
@@ -183,8 +183,15 @@ module.exports = (crowi) => {
       if (proxyServerUri != null) {
       if (proxyServerUri != null) {
         try {
         try {
           if (settings.slackAppIntegrations.length > 0) {
           if (settings.slackAppIntegrations.length > 0) {
-            const tokenGtoPs = settings.slackAppIntegrations.map(slackAppIntegration => slackAppIntegration.tokenGtoP);
-            connectionStatuses = (await getConnectionStatusesFromProxy(tokenGtoPs)).connectionStatuses;
+            // key: slackAppIntegration.tokenGtoP, value: slackAppIntegration._id
+            const tokenGtoPToSlackAppIntegrationId = {};
+            settings.slackAppIntegrations.forEach((slackAppIntegration) => {
+              tokenGtoPToSlackAppIntegrationId[slackAppIntegration.tokenGtoP] = slackAppIntegration._id;
+            });
+            const result = (await getConnectionStatusesFromProxy(Object.keys(tokenGtoPToSlackAppIntegrationId)));
+            Object.entries(result.connectionStatuses).forEach(([tokenGtoP, connectionStatus]) => {
+              connectionStatuses[tokenGtoPToSlackAppIntegrationId[tokenGtoP]] = connectionStatus;
+            });
           }
           }
         }
         }
         catch (error) {
         catch (error) {
@@ -383,26 +390,16 @@ module.exports = (crowi) => {
       return res.apiv3Err(new ErrorV3(msg, 'create-slackAppIntegeration-failed'), 500);
       return res.apiv3Err(new ErrorV3(msg, 'create-slackAppIntegeration-failed'), 500);
     }
     }
 
 
-    let checkTokens;
-    let tokenGtoP;
-    let tokenPtoG;
-    let generateTokens;
-    // TODO: refactering generateAccessTokens by GW-6100
-    do {
-      generateTokens = SlackAppIntegration.generateAccessToken();
-      tokenGtoP = generateTokens[0];
-      tokenPtoG = generateTokens[1];
-      // eslint-disable-next-line no-await-in-loop
-      checkTokens = await SlackAppIntegration.findOne({ $or: [{ tokenGtoP }, { tokenPtoG }] });
-    } while (checkTokens != null);
+    const { tokenGtoP, tokenPtoG } = await SlackAppIntegration.generateUniqueAccessTokens();
+
     try {
     try {
       const slackAppTokens = await SlackAppIntegration.create({ tokenGtoP, tokenPtoG });
       const slackAppTokens = await SlackAppIntegration.create({ tokenGtoP, tokenPtoG });
       return res.apiv3(slackAppTokens, 200);
       return res.apiv3(slackAppTokens, 200);
     }
     }
     catch (error) {
     catch (error) {
-      const msg = 'Error occured in updating access token for slack app tokens';
+      const msg = 'Error occurred during creating slack integration settings procedure';
       logger.error('Error', error);
       logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(msg, 'update-slackAppTokens-failed'), 500);
+      return res.apiv3Err(new ErrorV3(msg, 'creating-slack-integration-settings-procedure-failed'), 500);
     }
     }
   });
   });
 
 
@@ -419,23 +416,20 @@ module.exports = (crowi) => {
    *          200:
    *          200:
    *            description: Succeeded to regenerate slack app tokens
    *            description: Succeeded to regenerate slack app tokens
    */
    */
-  // TODO: refactering generateAccessTokens by GW-6100
   router.put('/regenerate-tokens', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
   router.put('/regenerate-tokens', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
 
 
     const { slackAppIntegrationId } = req.body;
     const { slackAppIntegrationId } = req.body;
 
 
     try {
     try {
-      const generateTokens = SlackAppIntegration.generateAccessToken();
-      const newTokenGtoP = generateTokens[0];
-      const newTokenPtoG = generateTokens[1];
-      const slackAppTokens = await SlackAppIntegration.findOneAndUpdate({ _id: slackAppIntegrationId }, { tokenGtoP: newTokenGtoP, tokenPtoG: newTokenPtoG });
+      const { tokenGtoP, tokenPtoG } = await SlackAppIntegration.generateUniqueAccessTokens();
+      const slackAppTokens = await SlackAppIntegration.findOneAndUpdate({ _id: slackAppIntegrationId }, { tokenGtoP, tokenPtoG });
 
 
       return res.apiv3(slackAppTokens, 200);
       return res.apiv3(slackAppTokens, 200);
     }
     }
     catch (error) {
     catch (error) {
-      const msg = 'Error occured in updating access token for slack app tokens';
+      const msg = 'Error occurred during regenerating slack app tokens';
       logger.error('Error', error);
       logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(msg, 'update-slackAppTokens-failed'), 500);
+      return res.apiv3Err(new ErrorV3(msg, 'regenerating-slackAppTokens-failed'), 500);
     }
     }
   });
   });
 
 
@@ -570,12 +564,9 @@ module.exports = (crowi) => {
     }
     }
 
 
     const slackBotToken = crowi.configManager.getConfig('crowi', 'slackbot:token');
     const slackBotToken = crowi.configManager.getConfig('crowi', 'slackbot:token');
-    try {
-      await testToSlack(slackBotToken);
-    }
-    catch (error) {
-      logger.error('Error', error);
-      return res.apiv3Err(new ErrorV3(`Error occured while testing. Cause: ${error.message}`, 'test-failed', error.stack));
+    const status = await getConnectionStatus(slackBotToken);
+    if (status.error != null) {
+      return res.apiv3Err(new ErrorV3(`Error occured while getting connection. ${status.error}`, 'send-message-failed'));
     }
     }
 
 
     const { channel } = req.body;
     const { channel } = req.body;