Browse Source

Merge remote-tracking branch 'origin/master' into support/make-app-monorepo

Yuki Takei 4 years ago
parent
commit
c57e249418
36 changed files with 587 additions and 69 deletions
  1. 11 2
      CHANGES.md
  2. 2 1
      package.json
  3. 1 0
      packages/app/config/webpack.common.js
  4. 1 0
      packages/app/package.json
  5. BIN
      packages/app/public/images/themes/hufflepuff/badger-dark.jpg
  6. BIN
      packages/app/public/images/themes/hufflepuff/badger-light.png
  7. BIN
      packages/app/public/images/themes/hufflepuff/badger-light3.png
  8. 10 1
      packages/app/resource/locales/en_US/admin/admin.json
  9. 10 1
      packages/app/resource/locales/ja_JP/admin/admin.json
  10. 10 1
      packages/app/resource/locales/zh_CN/admin/admin.json
  11. 2 0
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  12. 1 1
      packages/app/src/components/Admin/Security/LdapAuthTest.jsx
  13. 6 2
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  14. 6 2
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx
  15. 6 3
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  16. 7 2
      packages/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  17. 12 3
      packages/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx
  18. 12 6
      packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  19. 2 2
      packages/app/src/components/Me/AssociateModal.jsx
  20. 0 1
      packages/app/src/server/crowi/express-init.js
  21. 12 0
      packages/app/src/server/crowi/index.js
  22. 32 0
      packages/app/src/server/middlewares/http-error-handler.js
  23. 1 1
      packages/app/src/server/routes/apiv3/pages.js
  24. 28 26
      packages/app/src/server/routes/apiv3/slack-integration-settings.js
  25. 1 0
      packages/app/src/server/service/app.js
  26. 310 0
      packages/app/src/styles/theme/hufflepuff.scss
  27. 1 0
      packages/slack/package.json
  28. 3 2
      packages/slack/src/middlewares/verify-growi-to-slack-request.ts
  29. 5 4
      packages/slack/src/middlewares/verify-slack-request.ts
  30. 1 0
      packages/slackbot-proxy/package.json
  31. 6 0
      packages/slackbot-proxy/src/Server.ts
  32. 9 8
      packages/slackbot-proxy/src/controllers/growi-to-slack.ts
  33. 22 0
      packages/slackbot-proxy/src/filters/CustomHttpErrorFilter.ts
  34. 28 0
      packages/slackbot-proxy/src/middlewares/GlobalHttpErrorHandlingMiddleware.ts
  35. 13 0
      packages/slackbot-proxy/src/models/errors.ts
  36. 16 0
      yarn.lock

+ 11 - 2
CHANGES.md

@@ -1,16 +1,25 @@
 # CHANGES
 
-## v4.3.1-RC
+## v4.3.2-RC
 
 * Update libs
     * @slack/web-api
     * morgan
+* 
+
+## v4.3.1
+
+* Fix: Build script for production
 
 ## v4.3.0-RC
 
 ### BREAKING CHANGES
 
-None.
+* GROWI manages dependencies with `lerna`
+    * Use `npx lerna bootstrap` instead of `yarn install`
+* GROWI includes some official plugins in default
+    * Users no longer need to install [growi-plugin-lsx](https://github.com/weseek/growi-plugin-lsx), [growi-plugin-pukiwiki-like-linker](https://github.com/weseek/growi-plugin-pukiwiki-like-linker) and [growi-plugin-attachment-refs](https://github.com/weseek/growi-plugin-attachment-refs) before build client.
+
 
 Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/43x.html>
 

+ 2 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.3.1-RC",
+  "version": "4.3.2-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -34,6 +34,7 @@
     "build:dev:watch": "npm run build:dev:app:watch",
     "build:dev:watch:poll": "npm run build:dev:app:watch:poll",
     "build:dev": "yarn build:dev:app",
+    "build:prod": "yarn lerna run build --scope @growi/app --scope @growi/slack",
     "build:slack": "lerna run build --scope @growi/slack",
     "build": "npm run build:dev:watch",
     "build:poll": "npm run build:dev:watch:poll",

+ 1 - 0
packages/app/config/webpack.common.js

@@ -45,6 +45,7 @@ module.exports = (options) => {
       'styles/theme-island':      './src/client/styles/scss/theme/island.scss',
       'styles/theme-antarctic':      './src/client/styles/scss/theme/antarctic.scss',
       'styles/theme-spring':         './src/client/styles/scss/theme/spring.scss',
+      'styles/theme-hufflepuff':         './src/client/styles/scss/theme/hufflepuff.scss',
       // styles for external services
       'styles/style-hackmd':          './src/client/styles/hackmd/style.scss',
     }, options.entry || {}), // Merge with env dependent settings

+ 1 - 0
packages/app/package.json

@@ -63,6 +63,7 @@
     "growi-plugin-lsx": "^4.0.3",
     "growi-plugin-pukiwiki-like-linker": "^3.1.0",
     "helmet": "^3.13.0",
+    "http-errors": "~1.6.2",
     "i18next": "^20.3.2",
     "i18next-express-middleware": "^2.0.0",
     "i18next-node-fs-backend": "^2.1.0",

BIN
packages/app/public/images/themes/hufflepuff/badger-dark.jpg


BIN
packages/app/public/images/themes/hufflepuff/badger-light.png


BIN
packages/app/public/images/themes/hufflepuff/badger-light3.png


+ 10 - 1
packages/app/resource/locales/en_US/admin/admin.json

@@ -339,7 +339,16 @@
       "integration_some_ws_is_not_complete": "Some work spaces is not linked"
     },
     "custom_bot_with_proxy_integration": "Custom Bot With Proxy Integration",
-    "official_bot_integration": "Official bot integration"
+    "official_bot_integration": "Official bot integration",
+    "docs_url": {
+      "slack_integration": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/",
+      "official_bot": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#official-bot-%E3%80%90recommended%E3%80%91",
+      "custom_bot_without_proxy": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#custom-bot-without-proxy",
+      "custom_bot_with_proxy": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#custom-bot-with-proxy",
+      "official_bot_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/official-bot-settings.html",
+      "custom_bot_without_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-without-proxy-settings.html",
+      "custom_bot_with_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
+    }
   },
   "user_management": {
     "invite_users": "Temporarily issue a new user",

+ 10 - 1
packages/app/resource/locales/ja_JP/admin/admin.json

@@ -337,7 +337,16 @@
 
     },
     "custom_bot_with_proxy_integration": "Custom bot with proxy 連携",
-    "official_bot_integration": "Official bot 連携"
+    "official_bot_integration": "Official bot 連携",
+    "docs_url": {
+      "slack_integration": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/",
+      "official_bot": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/#official-bot-%E6%8E%A8%E5%A5%A8",
+      "custom_bot_without_proxy": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/#custom-bot-without-proxy",
+      "custom_bot_with_proxy": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/#custom-bot-with-proxy",
+      "official_bot_setting": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/official-bot-settings.html",
+      "custom_bot_without_proxy_setting": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/custom-bot-without-proxy-settings.html",
+      "custom_bot_with_proxy_setting": "https://docs.growi.org/ja/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
+    }
   },
   "user_management": {
     "invite_users": "新規ユーザーの仮発行",

+ 10 - 1
packages/app/resource/locales/zh_CN/admin/admin.json

@@ -346,7 +346,16 @@
       "integration_some_ws_is_not_complete": "有的工作空间未能连接"
     },
     "custom_bot_with_proxy_integration": "Custom bot with proxy 一体化",
-    "official_bot_integration": "Official bot 一体化"
+    "official_bot_integration": "Official bot 一体化",
+    "docs_url": {
+      "slack_integration": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/",
+      "official_bot": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#official-bot-%E3%80%90recommended%E3%80%91",
+      "custom_bot_without_proxy": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#custom-bot-without-proxy",
+      "custom_bot_with_proxy": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#custom-bot-with-proxy",
+      "official_bot_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/official-bot-settings.html",
+      "custom_bot_without_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-without-proxy-settings.html",
+      "custom_bot_with_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
+    }
   },
 	"user_management": {
 		"invite_users": "临时发布新用户",

+ 2 - 0
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -20,6 +20,8 @@ class CustomizeThemeOptions extends React.Component {
       name: 'default',    bg: '#ffffff', topbar: '#2a2929', sidebar: '#122c55', theme: '#209fd8',
     }, {
       name: 'mono-blue',  bg: '#F7FBFD', topbar: '#2a2929', sidebar: '#00587A', theme: '#00587A',
+    }, {
+      name: 'hufflepuff',  bg: '#EFE2CF', topbar: '#2a2929', sidebar: '#EAAB20', theme: '#993439',
     }];
 
     const uniqueTheme = [{

+ 1 - 1
packages/app/src/components/Admin/Security/LdapAuthTest.jsx

@@ -114,7 +114,7 @@ class LdapAuthTest extends React.Component {
 
         <div className="form-group">
           <label><h5>Logs</h5></label>
-          <textarea id="taLogs" className="col" rows="4" value={this.state.logs} readOnly />
+          <textarea id="taLogs" className="col form-control" rows="4" value={this.state.logs} readOnly />
         </div>
 
         <div>

+ 6 - 2
packages/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -66,8 +66,12 @@ const CustomBotWithProxySettings = (props) => {
   return (
     <>
       <h2 className="admin-setting-header mb-2">{t('admin:slack_integration.custom_bot_with_proxy_integration')}
-        {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
-        {/* <i className="fa fa-external-link btn-link ml-2" aria-hidden="true"></i> */}
+        <a href={t('admin:slack_integration.docs_url.custom_bot_with_proxy')} target="_blank" rel="noopener noreferrer">
+          <i
+            className="fa fa-external-link btn-link ml-2"
+            aria-hidden="true"
+          />
+        </a>
       </h2>
 
       {slackAppIntegrations.length !== 0 && (

+ 6 - 2
packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx

@@ -21,8 +21,12 @@ const CustomBotWithoutProxySettings = (props) => {
   return (
     <>
       <h2 className="admin-setting-header">{t('admin:slack_integration.custom_bot_without_proxy_integration')}
-        {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
-        {/* <i className="fa fa-external-link btn-link ml-2" aria-hidden="true"></i> */}
+        <a href={t('admin:slack_integration.docs_url.custom_bot_without_proxy')} target="_blank" rel="noopener noreferrer">
+          <i
+            className="fa fa-external-link btn-link ml-2"
+            aria-hidden="true"
+          />
+        </a>
       </h2>
 
       <CustomBotWithoutProxyConnectionStatus

+ 6 - 3
packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -76,15 +76,18 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
             {t('admin:slack_integration.accordion.create_bot')}
             <i className="fa fa-external-link ml-2" aria-hidden="true" />
           </button>
-          {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
-          {/* <a href="#">
+          <a
+            href={t('admin:slack_integration.docs_url.custom_bot_without_proxy_setting')}
+            target="_blank"
+            rel="noopener noreferrer"
+          >
             <p className="text-center mt-1">
               <small>
                 {t('admin:slack_integration.accordion.how_to_create_a_bot')}
                 <i className="fa fa-external-link ml-2" aria-hidden="true" />
               </small>
             </p>
-          </a> */}
+          </a>
         </div>
       </Accordion>
       <Accordion

+ 7 - 2
packages/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx

@@ -47,8 +47,13 @@ const OfficialBotSettings = (props) => {
   return (
     <>
       <h2 className="admin-setting-header">{t('admin:slack_integration.official_bot_integration')}
-        {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
-        {/* <i className="fa fa-external-link btn-link ml-2" aria-hidden="true"></i> */}
+        <a href={t('admin:slack_integration.docs_url.official_bot')} target="_blank" rel="noopener noreferrer">
+          <i
+            className="fa fa-external-link btn-link ml-2"
+            aria-hidden="true"
+            onClick={() => window.open(`${t('admin:slack_integration.docs_url.official_bot')}`, '_blank')}
+          />
+        </a>
       </h2>
 
       {slackAppIntegrations.length !== 0 && (

+ 12 - 3
packages/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -28,6 +28,8 @@ const SlackIntegration = (props) => {
   const [slackAppIntegrations, setSlackAppIntegrations] = useState();
   const [proxyServerUri, setProxyServerUri] = useState();
   const [connectionStatuses, setConnectionStatuses] = useState({});
+  const [errorMsg, setErrorMsg] = useState(null);
+  const [errorCode, setErrorCode] = useState(null);
   const [isLoading, setIsLoading] = useState(true);
 
 
@@ -38,6 +40,8 @@ const SlackIntegration = (props) => {
         slackSigningSecret, slackBotToken, slackSigningSecretEnvVars, slackBotTokenEnvVars, slackAppIntegrations, proxyServerUri,
       } = data.settings;
 
+      setErrorMsg(data.errorMsg);
+      setErrorCode(data.errorCode);
       setConnectionStatuses(data.connectionStatuses);
       setCurrentBotType(data.currentBotType);
       setSlackSigningSecret(slackSigningSecret);
@@ -187,13 +191,18 @@ const SlackIntegration = (props) => {
       <div className="selecting-bot-type mb-5">
         <h2 className="admin-setting-header mb-4">
           {t('admin:slack_integration.selecting_bot_types.slack_bot')}
-          {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
-          {/* <a className="ml-2 btn-link" href="#">
+          <a className="ml-2 btn-link" href={t('admin:slack_integration.docs_url.slack_integration')} target="_blank" rel="noopener noreferrer">
             {t('admin:slack_integration.selecting_bot_types.detailed_explanation')}
             <i className="fa fa-external-link ml-1" aria-hidden="true"></i>
-          </a> */}
+          </a>
         </h2>
 
+        { errorCode && (
+          <div className="alert alert-warning">
+            <strong>ERROR: </strong>{errorMsg} ({errorCode})
+          </div>
+        ) }
+
         <div className="d-flex justify-content-end">
           <button
             className="btn btn-outline-danger"

+ 12 - 6
packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx

@@ -22,15 +22,18 @@ const BotCreateProcess = () => {
         {t('admin:slack_integration.accordion.create_bot')}
         <i className="fa fa-external-link ml-2" aria-hidden="true" />
       </button>
-      {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
-      {/* <a href="#">
+      <a
+        href={t('admin:slack_integration.docs_url.custom_bot_with_proxy_setting')}
+        target="_blank"
+        rel="noopener noreferrer"
+      >
         <p className="text-center mt-1">
           <small>
             {t('admin:slack_integration.accordion.how_to_create_a_bot')}
             <i className="fa fa-external-link ml-2" aria-hidden="true" />
           </small>
         </p>
-      </a> */}
+      </a>
     </div>
   );
 };
@@ -43,15 +46,18 @@ const BotInstallProcessForOfficialBot = () => {
         {t('admin:slack_integration.accordion.install_now')}
         <i className="fa fa-external-link ml-2" aria-hidden="true" />
       </button>
-      {/* TODO: If Bot-manual section of docs is merged into master, show links and add an appropriate links by GW-5614. */}
-      {/* <a href="#">
+      <a
+        href={t('admin:slack_integration.docs_url.official_bot_setting')}
+        target="_blank"
+        rel="noopener noreferrer"
+      >
         <p className="text-center mt-1">
           <small>
             {t('admin:slack_integration.accordion.how_to_install')}
             <i className="fa fa-external-link ml-2" aria-hidden="true" />
           </small>
         </p>
-      </a> */}
+      </a>
     </div>
   );
 };

+ 2 - 2
packages/app/src/components/Me/AssociateModal.jsx

@@ -71,7 +71,7 @@ class AssociateModal extends React.Component {
 
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} size="lg">
-        <ModalHeader className="bg-info text-light" toggle={this.props.onClose}>
+        <ModalHeader className="bg-primary text-light" toggle={this.props.onClose}>
           { t('admin:user_management.create_external_account') }
         </ModalHeader>
         <ModalBody>
@@ -118,7 +118,7 @@ class AssociateModal extends React.Component {
           </div>
         </ModalBody>
         <ModalFooter className="border-top-0">
-          <button type="button" className="btn btn-info mt-3" onClick={this.onClickAddBtn}>
+          <button type="button" className="btn btn-primary mt-3" onClick={this.onClickAddBtn}>
             <i className="fa fa-plus-circle" aria-hidden="true"></i>
             {t('add')}
           </button>

+ 0 - 1
packages/app/src/server/crowi/express-init.js

@@ -1,5 +1,4 @@
 
-
 module.exports = function(crowi, app) {
   const debug = require('debug')('growi:crowi:express-init');
   const path = require('path');

+ 12 - 0
packages/app/src/server/crowi/index.js

@@ -13,6 +13,7 @@ const mongoose = require('mongoose');
 const models = require('../models');
 
 const PluginService = require('../plugins/plugin.service');
+const httpErrorHandler = require('../middlewares/http-error-handler');
 
 const sep = path.sep;
 
@@ -451,6 +452,9 @@ Crowi.prototype.start = async function() {
   // setup Express Routes
   this.setupRoutesAtLast();
 
+  // setup Global Error Handlers
+  this.setupGlobalErrorHandlers();
+
   return serverListening;
 };
 
@@ -486,6 +490,14 @@ Crowi.prototype.setupRoutesAtLast = function() {
   require('../routes')(this, this.express);
 };
 
+/**
+ * setup global error handlers
+ * !! this must be after the Routes setup !!
+ */
+Crowi.prototype.setupGlobalErrorHandlers = function() {
+  this.express.use(httpErrorHandler);
+};
+
 /**
  * require API for plugins
  *

+ 32 - 0
packages/app/src/server/middlewares/http-error-handler.js

@@ -0,0 +1,32 @@
+const { HttpError } = require('../../../node_modules/http-errors');
+
+const isHttpError = (val) => {
+  if (!val || typeof val !== 'object') {
+    return false;
+  }
+
+  if (val instanceof HttpError) {
+    return true;
+  }
+
+  return val instanceof Error
+    && typeof val.expose === 'boolean'
+    && typeof val.statusCode === 'number'
+    && val.status === val.statusCode;
+};
+
+module.exports = async(err, req, res, next) => {
+  // handle if the err is a HttpError instance
+  if (isHttpError(err)) {
+    const httpError = err;
+
+    return res
+      .status(httpError.status)
+      .send({
+        status: httpError.status,
+        message: httpError.message,
+      });
+  }
+
+  next(err);
+};

+ 1 - 1
packages/app/src/server/routes/apiv3/pages.js

@@ -283,7 +283,7 @@ module.exports = (crowi) => {
    *            description: Return pages recently updated
    *
    */
-  router.get('/recent', loginRequired, async(req, res) => {
+  router.get('/recent', accessTokenParser, loginRequired, async(req, res) => {
     const limit = 20;
     const offset = parseInt(req.query.offset) || 0;
 

+ 28 - 26
packages/app/src/server/routes/apiv3/slack-integration-settings.js

@@ -153,8 +153,10 @@ module.exports = (crowi) => {
 
     // retrieve connection statuses
     let connectionStatuses = {};
+    let errorMsg;
+    let errorCode;
     if (currentBotType == null) {
-      // TODO imple null action
+      // no need to do anything
     }
     else if (currentBotType === 'customBotWithoutProxy') {
       const token = settings.slackBotToken;
@@ -163,10 +165,10 @@ module.exports = (crowi) => {
         try {
           connectionStatuses = await getConnectionStatuses([token]);
         }
-        catch (error) {
-          const msg = 'Error occured in getting connection statuses';
-          logger.error('Error', error);
-          return res.apiv3Err(new ErrorV3(msg, 'get-connection-failed'), 500);
+        catch (e) {
+          errorMsg = 'Error occured in getting connection statuses';
+          errorCode = 'get-connection-failed';
+          logger.error(errorMsg, e);
         }
       }
     }
@@ -175,37 +177,37 @@ module.exports = (crowi) => {
         const slackAppIntegrations = await SlackAppIntegration.find();
         settings.slackAppIntegrations = slackAppIntegrations;
       }
-      catch (error) {
-        const msg = 'Error occured in getting connection statuses';
-        logger.error('Error', error);
-        return res.apiv3Err(new ErrorV3(msg, 'get-connection-failed'), 500);
+      catch (e) {
+        errorMsg = 'Error occured in finding SlackAppIntegration entities.';
+        errorCode = 'get-slackappintegration-failed';
+        logger.error(errorMsg, e);
       }
 
       const proxyServerUri = settings.proxyServerUri;
 
-      if (proxyServerUri != null) {
+      if (proxyServerUri != null && settings.slackAppIntegrations != null && settings.slackAppIntegrations.length > 0) {
         try {
-          if (settings.slackAppIntegrations.length > 0) {
-            // 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;
-            });
-          }
+          // 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) {
-          const msg = 'Incorrect Proxy URL';
-          logger.error('Error', error);
-          return res.apiv3Err(new ErrorV3(msg, 'test-connection-failed'), 400);
+        catch (e) {
+          errorMsg = 'Incorrect Proxy URL';
+          errorCode = 'test-connection-failed';
+          logger.error(errorMsg, e);
         }
       }
     }
 
-    return res.apiv3({ currentBotType, settings, connectionStatuses });
+    return res.apiv3({
+      currentBotType, settings, connectionStatuses, errorMsg, errorCode,
+    });
   });
 
   /**

+ 1 - 0
packages/app/src/server/service/app.js

@@ -113,6 +113,7 @@ class AppService extends S2sMessageHandlable {
   async setupAfterInstall() {
     this.crowi.pluginService.autoDetectAndLoadPlugins();
     this.crowi.setupRoutesAtLast();
+    this.crowi.setupGlobalErrorHandlers();
 
     // remove message handler
     const { s2sMessagingService } = this;

+ 310 - 0
packages/app/src/styles/theme/hufflepuff.scss

@@ -0,0 +1,310 @@
+@import '../variables';
+@import '../override-bootstrap-variables';
+
+// == Define Bootstrap theme colors
+//
+
+// colors for overriding bootstrap $theme-colors
+// $secondary: #;
+// $info: #;
+// $success: #;
+// $warning: #;
+// $danger: #;
+// $light: #;
+// $dark: #;
+
+// .grw-navbar {
+//   border-bottom: $accentcolor 4px solid;
+// }
+
+//== Light Mode
+//
+html[light] {
+  // Theme colors
+  $themecolor: #eaab20;
+  $themelight: #efe2cf;
+  $subthemecolor: #231e1d;
+  $third-main-color: #f0c05a;
+  $accentcolor: #993439;
+
+  $primary: $themecolor;
+  // $secondary: $subthemecolor;
+  $secondary: $third-main-color;
+
+  // Background colors
+  $bgcolor-global: lighten($themelight, 10%);
+  $bgcolor-inline-code: $gray-100; //optional
+  $bgcolor-card: $gray-100;
+  $bgcolor-highlighted: rgba($primary, 0.5);
+
+  // Font colors
+  $color-global: $subthemecolor;
+  $color-reversal: white;
+  $color-link: $accentcolor;
+  $color-link-hover: lighten($accentcolor, 10%);
+  $color-link-wiki: $accentcolor;
+  $color-link-wiki-hover: lighten($color-link-wiki, 10%);
+  $color-link-nabvar: $color-reversal;
+  $color-inline-code: #c7254e; // optional
+
+  // List Group colors
+  // $color-list: $color-global;
+  $bgcolor-list: transparent;
+  $color-list-hover: lighten($themecolor, 10%);
+  // $bgcolor-list-hover: darken($bgcolor-list, 2%);
+  // $color-list-active: $bgcolor-global;
+  // $bgcolor-list-active: $accentcolor;
+
+  // Navbar
+  $bgcolor-navbar: $third-main-color;
+  $bgcolor-search-top-dropdown: $themecolor;
+  $border-image-navbar: linear-gradient(to right, #90a555 0%, #a84be6 50%, #eaab20 100%);
+
+  // Logo colors
+  $bgcolor-logo: $bgcolor-navbar;
+  $fillcolor-logo-mark: lighten(desaturate($bgcolor-inline-code, 10%), 15%);
+
+  // Sidebar
+  $bgcolor-sidebar: $themecolor;
+  // Sidebar resize button
+  $color-resize-button: $color-reversal;
+  $bgcolor-resize-button: $subthemecolor;
+  $color-resize-button-hover: $color-reversal;
+  $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 10%);
+  // Sidebar contents
+  $color-sidebar-context: $accentcolor;
+  $bgcolor-sidebar-context: lighten($themelight, 8%);
+  // Sidebar list group
+  $bgcolor-sidebar-list-group: lighten($themelight, 10%);
+
+  // Icon colors
+  $color-editor-icons: $accentcolor;
+
+  // Border colors
+  $border-color-theme: lighten($subthemecolor, 40%);
+  $bordercolor-inline-code: #ccc8c8; // optional
+
+  // Dropdown colors
+  $bgcolor-dropdown-link-active: $growi-blue;
+  $color-dropdown-link-active: $color-reversal;
+  $color-dropdown-link-hover: $color-global;
+
+  // admin theme box
+  $color-theme-color-box: darken($primary, 5%);
+
+  @import 'apply-colors';
+  @import 'apply-colors-light';
+
+  //Button
+  .btn.btn-outline-primary {
+    @include btn-page-editor-mode-manager(darken($primary, 50%), darken($primary, 50%), lighten($primary, 20%));
+  }
+  .btn-group.grw-page-editor-mode-manager {
+    .btn.btn-outline-primary {
+      @include btn-page-editor-mode-manager(darken($primary, 70%), lighten($primary, 5%), lighten($primary, 20%));
+    }
+  }
+
+  .growi:not(.login-page) {
+    // add background-image
+    #page-wrapper,
+    .page-editor-preview-container {
+      background-image: url('/images/themes/hufflepuff/badger-light3.png');
+      background-attachment: fixed;
+      background-position: bottom;
+      background-size: cover;
+    }
+  }
+
+  // login and register
+  .nologin {
+    #page-wrapper {
+      background-color: $themelight;
+      background-image: url('/images/themes/hufflepuff/badger-light.png');
+      background-attachment: fixed;
+      background-position: bottom;
+      background-size: cover;
+    }
+
+    .login-header,
+    .login-dialog {
+      background-color: rgba(black, 0.1);
+    }
+
+    .link-switch {
+      color: $color-global;
+    }
+
+    .grw-external-auth-form {
+      border-color: $accentcolor !important;
+    }
+  }
+
+  .table {
+    background-color: $bgcolor-global;
+  }
+
+  .card-timeline > .card-header {
+    background-color: $third-main-color;
+  }
+
+  .nav.nav-tabs {
+    > .nav-item {
+      > .nav-link.active {
+        color: $subthemecolor;
+      }
+    }
+  }
+}
+
+html[dark] {
+  // Theme colors
+  $themecolor: #eaab20;
+  $themedark: #3d3f38;
+  $subthemecolor: #231e1d;
+  $third-main-color: #967224;
+  $accentcolor: #993439;
+
+  $primary: darken($themecolor, 10%);
+  $secondary: $third-main-color;
+  $dark: #031018;
+
+  // Background colors
+  $bgcolor-global: $themedark;
+  // $bgcolor-navbar: #27343b;
+  $bgcolor-inline-code: $subthemecolor;
+  $bgcolor-card: darken($themedark, 5%);
+  $bgcolor-highlighted: rgba($primary, 0.5);
+
+  // Font colors
+  $color-global: #efe2cf;
+  $color-reversal: $gray-100;
+  $color-link: lighten($themecolor, 20%);
+  $color-link-hover: lighten($color-link, 10%);
+  $color-link-wiki: lighten($primary, 20%);
+  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
+  $color-link-nabvar: $color-reversal;
+  $color-inline-code: $themecolor;
+  // $color-inline-code: #c7254e; // optional
+  // $color-search: #000102;
+
+  // List Group colors
+  // $color-list: $color-global;
+  $bgcolor-list: transparent;
+  $color-list-hover: $accentcolor;
+  // $bgcolor-list-hover: lighten($bgcolor-global, 3%);
+  // $color-list-active: $color-reversal;
+  // $bgcolor-list-active: $primary;
+
+  // Navbar
+  $bgcolor-navbar: $third-main-color;
+  $bgcolor-search-top-dropdown: $themecolor;
+  $border-image-navbar: linear-gradient(to right, #90a555 0%, #3d98a3 50%, #eaab20 100%);
+
+  // Logo colors
+  $bgcolor-logo: #13191c;
+  $fillcolor-logo-mark: white;
+
+  // Sidebar
+  $bgcolor-sidebar: $themecolor;
+  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.3); // optional
+  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #cc951e; // optional
+  // Sidebar resize button
+  $color-resize-button: $color-global;
+  $bgcolor-resize-button: $accentcolor;
+  $color-resize-button-hover: $color-global;
+  $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 7%);
+  // Sidebar contents
+  $color-sidebar-context: $color-global;
+  $bgcolor-sidebar-context: $subthemecolor;
+  // Sidebar list group
+  $bgcolor-sidebar-list-group: lighten($subthemecolor, 5%);
+
+  // Icon colors
+  $color-editor-icons: $themecolor;
+
+  // Border colors
+  $border-color-theme: darken($themecolor, 25%);
+  $bordercolor-inline-code: #4d4d4d; // optional
+
+  // Dropdown colors
+  $bgcolor-dropdown-link-active: $primary;
+  $color-dropdown-link-active: $color-global;
+  $color-dropdown-link-hover: $color-reversal;
+
+  // admin theme box
+  $color-theme-color-box: $primary;
+
+  @import 'apply-colors';
+  @import 'apply-colors-dark';
+
+  // Navs
+  .nav-tabs {
+    border-bottom: $border-color-theme 1px solid;
+    .nav-link {
+      &:hover {
+        border-color: lighten($border-color-theme, 10%);
+        border-bottom: none;
+      }
+      &.active {
+        color: $color-link;
+        background-color: transparent;
+        border-color: $border-color-theme;
+      }
+    }
+  }
+
+  // Table
+  .table {
+    color: white;
+  }
+
+  // Button
+  .btn.btn-outline-primary {
+    @include btn-page-editor-mode-manager(lighten($primary, 40%), lighten($primary, 15%), darken($primary, 10%), darken($primary, 30%));
+  }
+  .btn-group.grw-page-editor-mode-manager {
+    .btn.btn-outline-primary {
+      @include btn-page-editor-mode-manager(lighten($primary, 40%), lighten($primary, 15%), darken($primary, 0%), darken($primary, 30%));
+    }
+  }
+
+  .card-timeline > .card-header {
+    background-color: $accentcolor;
+  }
+
+  .growi:not(.login-page) {
+    // add background-image
+    #page-wrapper,
+    .page-editor-preview-container {
+      background-image: url('/images/themes/hufflepuff/badger-dark.jpg');
+      background-attachment: fixed;
+      background-position: bottom;
+      background-size: cover;
+    }
+  }
+
+  // login and register
+  .nologin {
+    #page-wrapper {
+      background-color: $themedark;
+      background-image: url('/images/themes/hufflepuff/badger-light.png');
+      background-attachment: fixed;
+      background-position: bottom;
+      background-size: cover;
+    }
+
+    .login-header,
+    .login-dialog {
+      background-color: rgba(black, 0.1);
+    }
+
+    .link-switch {
+      color: $color-global;
+    }
+
+    .grw-external-auth-form {
+      border-color: $accentcolor !important;
+    }
+  }
+}

+ 1 - 0
packages/slack/package.json

@@ -20,6 +20,7 @@
     "bunyan": "^1.8.15",
     "dotenv-flow": "^3.2.0",
     "extensible-custom-error": "^0.0.7",
+    "http-errors": "^1.8.0",
     "universal-bunyan": "^0.9.2"
   },
   "devDependencies": {

+ 3 - 2
packages/slack/src/middlewares/verify-growi-to-slack-request.ts

@@ -1,5 +1,6 @@
 import { Response, NextFunction } from 'express';
 
+import createError from 'http-errors';
 import loggerFactory from '../utils/logger';
 import { RequestFromGrowi } from '../interfaces/request-between-growi-and-proxy';
 
@@ -15,14 +16,14 @@ export const verifyGrowiToSlackRequest = (req: RequestFromGrowi, res: Response,
   if (str == null) {
     const message = 'The value of header \'x-growi-gtop-tokens\' must not be empty.';
     logger.warn(message, { body: req.body });
-    return res.status(400).send({ message });
+    return next(createError(400, message));
   }
 
   const tokens = str.split(',').map(value => value.trim());
   if (tokens.length === 0) {
     const message = 'The value of header \'x-growi-gtop-tokens\' must include at least one or more tokens.';
     logger.warn(message, { body: req.body });
-    return res.status(400).send({ message });
+    return next(createError(400, message));
   }
 
   req.tokenGtoPs = tokens;

+ 5 - 4
packages/slack/src/middlewares/verify-slack-request.ts

@@ -2,6 +2,7 @@ import { createHmac, timingSafeEqual } from 'crypto';
 import { stringify } from 'qs';
 import { Response, NextFunction } from 'express';
 
+import createError from 'http-errors';
 import loggerFactory from '../utils/logger';
 import { RequestFromSlack } from '../interfaces/request-from-slack';
 
@@ -17,7 +18,7 @@ export const verifySlackRequest = (req: RequestFromSlack, res: Response, next: N
   if (signingSecret == null) {
     const message = 'No signing secret.';
     logger.warn(message, { body: req.body });
-    return res.status(400).send({ message });
+    return next(createError(400, message));
   }
 
   // take out slackSignature and timestamp from header
@@ -27,7 +28,7 @@ export const verifySlackRequest = (req: RequestFromSlack, res: Response, next: N
   if (slackSignature == null || timestamp == null) {
     const message = 'Forbidden. Enter from Slack workspace';
     logger.warn(message, { body: req.body });
-    return res.status(403).send({ message });
+    return next(createError(403, message));
   }
 
   // protect against replay attacks
@@ -35,7 +36,7 @@ export const verifySlackRequest = (req: RequestFromSlack, res: Response, next: N
   if (Math.abs(time - timestamp) > 300) {
     const message = 'Verification failed.';
     logger.warn(message, { body: req.body });
-    return res.status(403).send({ message });
+    return next(createError(403, message));
   }
 
   // generate growi signature
@@ -52,5 +53,5 @@ export const verifySlackRequest = (req: RequestFromSlack, res: Response, next: N
 
   const message = 'Verification failed.';
   logger.warn(message, { body: req.body });
-  return res.status(403).send({ message });
+  return next(createError(403, message));
 };

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

@@ -47,6 +47,7 @@
     "express-graceful-exit": "=0.5.0",
     "extensible-custom-error": "^0.0.7",
     "helmet": "^4.6.0",
+    "http-errors": "^1.8.0",
     "method-override": "^3.0.0",
     "mysql2": "^2.2.5",
     "typeorm": "^0.2.31",

+ 6 - 0
packages/slackbot-proxy/src/Server.ts

@@ -18,6 +18,8 @@ import { createTerminus } from '@godaddy/terminus';
 
 import swaggerSettingsForDev from '~/config/swagger/config.dev';
 import swaggerSettingsForProd from '~/config/swagger/config.prod';
+import { GlobalHttpErrorHandlingMiddleware } from './middlewares/GlobalHttpErrorHandlingMiddleware';
+import './filters/CustomHttpErrorFilter';
 import './filters/ResourceNotFoundFilter';
 import loggerFactory from '~/utils/logger';
 
@@ -146,6 +148,10 @@ export class Server {
     this.setupLogger();
   }
 
+  $afterRoutesInit(): void {
+    this.app.use(GlobalHttpErrorHandlingMiddleware);
+  }
+
   $beforeListen(): void {
     const expressApp = this.app.getApp();
     const server = this.injector.get<HttpServer>(HttpServer);

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

@@ -2,6 +2,7 @@ import {
   Controller, Get, Post, Inject, Req, Res, UseBefore, PathParams,
 } from '@tsed/common';
 import axios from 'axios';
+import createError from 'http-errors';
 
 import { WebAPICallResult } from '@slack/web-api';
 
@@ -99,7 +100,7 @@ export class GrowiToSlackCtrl {
     const { tokenGtoPs } = req;
 
     if (tokenGtoPs.length !== 1) {
-      return res.status(400).send({ message: 'installation is invalid' });
+      throw createError(400, 'installation is invalid');
     }
 
     const tokenGtoP = tokenGtoPs[0];
@@ -116,7 +117,7 @@ export class GrowiToSlackCtrl {
 
       const token = relation.installation.data.bot?.token;
       if (token == null) {
-        return res.status(400).send({ message: 'installation is invalid' });
+        throw createError(400, 'installation is invalid');
       }
 
       try {
@@ -124,12 +125,12 @@ export class GrowiToSlackCtrl {
       }
       catch (err) {
         logger.error(err);
-        return res.status(400).send({ message: `failed to request to GROWI. err: ${err.message}` });
+        throw createError(400, `failed to request to GROWI. 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}` });
+        throw createError(400, `failed to get connection. err: ${status.error}`);
       }
 
       return res.send({ relation, slackBotToken: token });
@@ -143,7 +144,7 @@ export class GrowiToSlackCtrl {
       .getOne();
 
     if (order == null || order.isExpired()) {
-      return res.status(400).send({ message: 'order has expired or does not exist.' });
+      throw createError(400, 'order has expired or does not exist.');
     }
 
     // Access the GROWI URL saved in the Order record and check if the GtoP token is valid.
@@ -152,19 +153,19 @@ export class GrowiToSlackCtrl {
     }
     catch (err) {
       logger.error(err);
-      return res.status(400).send({ message: `failed to request to GROWI. err: ${err.message}` });
+      throw createError(400, `failed to request to GROWI. err: ${err.message}`);
     }
 
     logger.debug('order found', order);
 
     const token = order.installation.data.bot?.token;
     if (token == null) {
-      return res.status(400).send({ message: 'installation is invalid' });
+      throw createError(400, 'installation is invalid');
     }
 
     const status = await getConnectionStatus(token);
     if (status.error != null) {
-      return res.status(400).send({ message: `failed to get connection. err: ${status.error}` });
+      throw createError(400, `failed to get connection. err: ${status.error}`);
     }
 
     logger.debug('relation test is success', order);

+ 22 - 0
packages/slackbot-proxy/src/filters/CustomHttpErrorFilter.ts

@@ -0,0 +1,22 @@
+import {
+  Catch, ExceptionFilterMethods, PlatformContext, PlatformResponse,
+} from '@tsed/common';
+
+import { CustomHttpError } from '~/models/errors';
+
+@Catch(CustomHttpError)
+export class CustomHttpErrorFilter implements ExceptionFilterMethods {
+
+  async catch(exception: CustomHttpError, ctx: PlatformContext): Promise<PlatformResponse<any>> {
+    const { httpError } = exception;
+    const { response } = ctx;
+
+    return response
+      .status(httpError.status)
+      .body({
+        status: httpError.status,
+        message: httpError.message,
+      });
+  }
+
+}

+ 28 - 0
packages/slackbot-proxy/src/middlewares/GlobalHttpErrorHandlingMiddleware.ts

@@ -0,0 +1,28 @@
+import {
+  Err, Middleware, Next, PlatformContext, PlatformResponse,
+} from '@tsed/common';
+
+import { HttpError, isHttpError } from 'http-errors';
+
+@Middleware()
+export class GlobalHttpErrorHandlingMiddleware {
+
+  use(@Err() err: unknown, @Next() next: Next, ctx: PlatformContext): PlatformResponse<any>|void {
+
+    // handle if the err is a HttpError instance
+    if (isHttpError(err)) {
+      const httpError = err as HttpError;
+      const { response } = ctx;
+
+      return response
+        .status(httpError.status)
+        .body({
+          status: httpError.status,
+          message: httpError.message,
+        });
+    }
+
+    next(err);
+  }
+
+}

+ 13 - 0
packages/slackbot-proxy/src/models/errors.ts

@@ -1,5 +1,7 @@
 import ExtensibleCustomError from 'extensible-custom-error';
 
+import { HttpError } from 'http-errors';
+
 export class InvalidUrlError extends ExtensibleCustomError {
 
   constructor(url: string) {
@@ -7,3 +9,14 @@ export class InvalidUrlError extends ExtensibleCustomError {
   }
 
 }
+
+export class CustomHttpError extends Error {
+
+  httpError: HttpError
+
+  constructor(httpError: HttpError) {
+    super(httpError.message);
+    this.httpError = httpError;
+  }
+
+}

+ 16 - 0
yarn.lock

@@ -9647,6 +9647,17 @@ http-errors@1.7.3, http-errors@~1.7.2:
     statuses ">= 1.5.0 < 2"
     toidentifier "1.0.0"
 
+http-errors@^1.8.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507"
+  integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.4"
+    setprototypeof "1.2.0"
+    statuses ">= 1.5.0 < 2"
+    toidentifier "1.0.0"
+
 http-proxy-agent@^4.0.0, http-proxy-agent@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"
@@ -17050,6 +17061,11 @@ setprototypeof@1.1.1:
   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
   integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
 
+setprototypeof@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
+  integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
+
 sha.js@^2.4.0, sha.js@^2.4.8:
   version "2.4.9"
   resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.9.tgz#98f64880474b74f4a38b8da9d3c0f2d104633e7d"