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

Merge branch 'master' into imprv/welcome-page

Yuki Takei 4 лет назад
Родитель
Сommit
7adfb16d3f
30 измененных файлов с 377 добавлено и 214 удалено
  1. 4 0
      CHANGES.md
  2. 1 1
      package.json
  3. 3 2
      packages/app/package.json
  4. 22 22
      packages/app/resource/locales/en_US/sandbox.md
  5. 10 0
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  6. 7 1
      packages/app/src/components/PageEditor/Editor.jsx
  7. 7 9
      packages/app/src/components/PageEditor/EditorIcon.jsx
  8. 6 1
      packages/app/src/server/crowi/express-init.js
  9. 2 2
      packages/app/src/server/routes/apiv3/healthcheck.js
  10. 2 2
      packages/app/src/server/routes/apiv3/search.js
  11. 2 2
      packages/app/src/server/routes/apiv3/statistics.js
  12. 3 0
      packages/slack/src/index.ts
  13. 3 3
      packages/slack/src/utils/check-communicable.ts
  14. 28 0
      packages/slack/src/utils/publish-initial-home-view.ts
  15. 10 0
      packages/slack/src/utils/required-scopes.ts
  16. 4 0
      packages/slack/src/utils/slash-command-parser.ts
  17. 21 0
      packages/slack/src/utils/welcome-message.ts
  18. 11 0
      packages/slackbot-proxy/CHANGES.md
  19. 4 1
      packages/slackbot-proxy/docker/Dockerfile
  20. 2 1
      packages/slackbot-proxy/package.json
  21. 9 1
      packages/slackbot-proxy/src/Server.ts
  22. 108 61
      packages/slackbot-proxy/src/controllers/slack.ts
  23. 2 9
      packages/slackbot-proxy/src/controllers/top.ts
  24. 32 0
      packages/slackbot-proxy/src/middlewares/slack-to-growi/join-to-conversation.ts
  25. 21 0
      packages/slackbot-proxy/src/views/install-failed.ejs
  26. 18 0
      packages/slackbot-proxy/src/views/install-succeeded-but-has-problem.ejs
  27. 21 0
      packages/slackbot-proxy/src/views/install-succeeded.ejs
  28. 1 0
      packages/slackbot-proxy/src/views/top.ejs
  29. 4 0
      packages/slackbot-proxy/tsconfig.base.json
  30. 9 96
      yarn.lock

+ 4 - 0
CHANGES.md

@@ -2,6 +2,7 @@
 
 ## v4.3.3-RC
 
+* Improvement: Add attachment button in editor navbar
 * Fix: Encode spaces in page path in LinkEditModal
 * Fix: Layout is broken when editing users page ([#4128](https://github.com/weseek/growi/issues/4128))
 * Support: Create @growi/core package
@@ -10,8 +11,11 @@
 * Support: Include official plugins as sub packages
 * Support: Upgrade libs
     * @slack/web-api
+    * date-fns
     * escape-string-regexp
+    * helmet
     * morgan
+    * socket.io
 
 ## v4.3.2
 

+ 1 - 1
package.json

@@ -31,7 +31,7 @@
   "scripts": {
     "start": "yarn app:server",
     "prestart": "yarn app:build",
-    "app:build": "yarn lerna run build --scope @growi/app --scope @growi/slack --scope @growi/plugin-pukiwiki-like-linker",
+    "app:build": "yarn lerna run build --scope @growi/app --scope @growi/slack --scope @growi/plugin-*",
     "app:server": "yarn lerna run server --scope @growi/app",
     "slackbot-proxy:build": "yarn lerna run build --scope @growi/slackbot-proxy --scope @growi/slack",
     "slackbot-proxy:server": "yarn lerna run start:prod --scope @growi/slackbot-proxy",

+ 3 - 2
packages/app/package.json

@@ -80,7 +80,7 @@
     "connect-redis": "^4.0.4",
     "cookie-parser": "^1.4.5",
     "csrf": "^3.1.0",
-    "date-fns": "^2.0.0",
+    "date-fns": "^2.23.0",
     "detect-indent": "^6.0.0",
     "diff": "^5.0.0",
     "elasticsearch": "^16.0.0",
@@ -96,7 +96,8 @@
     "express-webpack-assets": "^0.1.0",
     "graceful-fs": "^4.1.11",
     "growi-commons": "^5.0.4",
-    "helmet": "^3.13.0",
+    "helmet": "^4.6.0",
+    "nocache": "^3.0.1",
     "http-errors": "~1.6.2",
     "i18next": "^20.3.2",
     "i18next-express-middleware": "^2.0.0",

+ 22 - 22
packages/app/resource/locales/en_US/sandbox.md

@@ -37,7 +37,7 @@ Add one `#` per level at the start of the line
 
 ## Block paragraph
 
-Pararaphs are created by inserting a newline character
+Paragraphs are created by inserting a newline character
 A paragraph can be created by pressing Enter at the end of the previous paragraph.
 
 ```
@@ -53,17 +53,17 @@ paragraph2
 ## Br new line
 
 Add two spaces before break.
-***This behaviour can be modified in the options menu.***
+***This behavior can be modified in the options menu.***
 
 ```
-hoge
-fuga(two spaces)
-piyo
+foo
+bar(two spaces)
+baz
 ```
 
-hoge
-fuga
-piyo
+foo
+bar
+baz
 
 ## Blockquotes
 
@@ -84,7 +84,7 @@ Add one `>` per level at the start of the line
 Wrap code with three back quotes or tildes.
 
 ```
-print 'hoge'
+print 'foo'
 ```
 
 ### Syntax highlight and file name
@@ -131,16 +131,16 @@ This is  `Inline Code`.
 Code blocks should be preceded by four spaces or one tab.
 
 ```
-    class Hoge
-        def hoge
-            print 'hoge'
+    class Foo
+        def foo
+            print 'foo'
         end
     end
 ```
 
-    class Hoge
-        def hoge
-            print 'hoge'
+    class Foo
+        def foo
+            print 'foo'
         end
     end
 
@@ -166,7 +166,7 @@ ___
 
 ### Italic
 
-To italicize text, add One asterisk or underscores before and after a word or phrase.
+To italicize text, add one asterisk or underscores before and after a word or phrase.
 
 ```
 This is *Italic* .
@@ -178,7 +178,7 @@ This is _Italic_ .
 
 ### Bold
 
-To bold text, add two asterisks or underscores before and after a word or phrase.
+To make text bold, add two asterisks or underscores before and after a word or phrase.
 
 ```
 This is **bold**.
@@ -263,7 +263,7 @@ Example of Bootstrap4 is[[here>./Bootstrap4]]
 
 ## Ul Bulleted list
 
-To create an unordered list, add dashes (-), asterisks (*), or plus signs (+) in front of line items. 
+To create an unordered list, add dashes (-), asterisks (*), or plus signs (+) in front of line items.
 Items can be nested using indentation.
 
 ```
@@ -286,7 +286,7 @@ Items can be nested using indentation.
 
 ## Ol Numbered List
 
-To create an ordered list, add line items with numbers followed by periods. 
+To create an ordered list, add line items with numbers followed by periods.
 The numbers don’t have to be in numerical order, but the list should start with the number one.
 
 ```
@@ -449,9 +449,9 @@ See [emojione](https://www.emojione.com/)
 
 # :heavy_plus_sign: More..
 
-- Try to attach Bootstrap4 Tags?
+- Want to attach Bootstrap4 Tags?
     - :arrow_right: [/Sandbox/Bootstrap4]
-- Try to draw Diagrams?
+- Want to draw Diagrams?
     - :arrow_right: [/Sandbox/Diagrams]
-- Try to write Math Formulas?
+- Want to write Math Formulas?
     - :arrow_right: [/Sandbox/Math]

+ 10 - 0
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -801,6 +801,15 @@ export default class CodeMirrorEditor extends AbstractEditor {
       >
         <EditorIcon icon="CheckList" />
       </Button>,
+      <Button
+        key="nav-item-attachment"
+        color={null}
+        size="sm"
+        title="Attachment"
+        onClick={this.props.onAddAttachmentButtonClicked}
+      >
+        <EditorIcon icon="Attachment" />
+      </Button>,
       <Button
         key="nav-item-link"
         color={null}
@@ -947,6 +956,7 @@ CodeMirrorEditor.propTypes = Object.assign({
   emojiStrategy: PropTypes.object,
   lineNumbers: PropTypes.bool,
   onMarkdownHelpButtonClicked: PropTypes.func,
+  onAddAttachmentButtonClicked: PropTypes.func,
 }, AbstractEditor.propTypes);
 CodeMirrorEditor.defaultProps = {
   lineNumbers: true,

+ 7 - 1
packages/app/src/components/PageEditor/Editor.jsx

@@ -39,6 +39,7 @@ export default class Editor extends AbstractEditor {
     this.dropHandler = this.dropHandler.bind(this);
 
     this.showMarkdownHelp = this.showMarkdownHelp.bind(this);
+    this.addAttachmentHandler = this.addAttachmentHandler.bind(this);
 
     this.getAcceptableType = this.getAcceptableType.bind(this);
     this.getDropzoneClassName = this.getDropzoneClassName.bind(this);
@@ -187,6 +188,10 @@ export default class Editor extends AbstractEditor {
     this.setState({ isCheatsheetModalShown: true });
   }
 
+  addAttachmentHandler() {
+    this.dropzone.open();
+  }
+
   getDropzoneClassName(isDragAccept, isDragReject) {
     let className = 'dropzone';
     if (!this.props.isUploadable) {
@@ -314,6 +319,7 @@ export default class Editor extends AbstractEditor {
                         onPasteFiles={this.pasteFilesHandler}
                         onDragEnter={this.dragEnterHandler}
                         onMarkdownHelpButtonClicked={this.showMarkdownHelp}
+                        onAddAttachmentButtonClicked={this.addAttachmentHandler}
                         {...this.props}
                       />
                     )}
@@ -341,7 +347,7 @@ export default class Editor extends AbstractEditor {
             <button
               type="button"
               className="btn btn-outline-secondary btn-block btn-open-dropzone"
-              onClick={() => { this.dropzone.open() }}
+              onClick={this.addAttachmentHandler}
             >
               <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
               Attach files

+ 7 - 9
packages/app/src/components/PageEditor/EditorIcon.jsx

@@ -118,15 +118,13 @@ const EditorIcon = (props) => {
           <path d="M22.12,17H19.75l-3.12-4H18a1,1,0,0,0,1-1V8a1,1,0,0,0-1-1H12a1,1,0,0,0-1,1v4a1,1,0,0,0,1,1h1.38l-2.92,4H7.88A.94.94,0,0,0,7,18v4a.94.94,0,0,0,.88,1h5.24A.94.94,0,0,0,14,22V18a.94.94,0,0,0-.88-1H11.63l3.13-4h.47l3.13,4H16.88A.94.94,0,0,0,16,18v4a.94.94,0,0,0,.88,1h5.24A.94.94,0,0,0,23,22V18A.94.94,0,0,0,22.12,17ZM13,22H8V18h5ZM12,8h6v4H12ZM22,22H17V18h5Z" />
         </svg>
       );
-    // Unused icon
-    // case 'attachment':
-    //   return (
-    //     <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
-    //       <rect fillOpacity="0" width="30" height="30" />
-    //       <path d="M9.71,22.5a2.57,2.57,0,0,1-1.85-.79,2.79,2.79,0,0,1,0-4l9-9.23a3.21,3.21,0,0,1,1.59-.87,3.39,3.39,0,0,1,1.81.1,4.38,4.38,0,0,1,1.7,1.05,4.15,4.15,0,0,1,.46.56,3.73,3.73,0,0,1,.35.65,4.25,4.25,0,0,1,.2.72,3.91,3.91,0,0,1,.07.76,3.71,3.71,0,0,1-1.12,2.67l-6.79,7a.48.48,0,0,1-.34.16.51.51,0,0,1-.35-.13.48.48,0,0,1,0-.7l6.78-7a2.8,2.8,0,0,0,.84-2,2.58,2.58,0,0,0-.79-2,3.63,3.63,0,0,0-1.11-.75,2.41,2.41,0,0,0-1.31-.17,2.19,2.19,0,0,0-1.25.62l-9,9.22A1.8,1.8,0,0,0,8,19.69,1.78,1.78,0,0,0,8.58,21a1.81,1.81,0,0,0,.57.39,1.48,1.48,0,0,0,.66.1,2,2,0,0,0,1.28-.62l7.12-7.35.15-.16a1.15,1.15,0,0,0,.15-.2.9.9,0,0,0,.12-.24,1.17,1.17,0,0,0,.07-.25.52.52,0,0,0-.05-.27.75.75,0,0,0-.19-.26.73.73,0,0,0-.58-.27,1.29,1.29,0,0,0-.67.38l-5.36,5.53a.5.5,0,0,1-.22.13.46.46,0,0,1-.26,0,.48.48,0,0,1-.22-.12A.41.41,0,0,1,11,17.5a.5.5,0,0,1,.14-.35L16.5,11.6a2.19,2.19,0,0,1,1.29-.67,1.69,1.69,0,0,1,1.37.55,1.54,1.54,0,0,1,.53,1.31,2.26,2.26,0,0,1-.76,1.42L11.8,21.58a3.06,3.06,0,0,1-2,.91H9.71Z" />
-    //     </svg>
-    // );
-
+    case 'Attachment':
+      return (
+        <svg xmlns="http://www.w3.org/2000/svg" height="30" viewBox="0 0 30 30">
+          <rect fillOpacity="0" width="30" height="30" />
+          <path d="M9.71,22.5a2.57,2.57,0,0,1-1.85-.79,2.79,2.79,0,0,1,0-4l9-9.23a3.21,3.21,0,0,1,1.59-.87,3.39,3.39,0,0,1,1.81.1,4.38,4.38,0,0,1,1.7,1.05,4.15,4.15,0,0,1,.46.56,3.73,3.73,0,0,1,.35.65,4.25,4.25,0,0,1,.2.72,3.91,3.91,0,0,1,.07.76,3.71,3.71,0,0,1-1.12,2.67l-6.79,7a.48.48,0,0,1-.34.16.51.51,0,0,1-.35-.13.48.48,0,0,1,0-.7l6.78-7a2.8,2.8,0,0,0,.84-2,2.58,2.58,0,0,0-.79-2,3.63,3.63,0,0,0-1.11-.75,2.41,2.41,0,0,0-1.31-.17,2.19,2.19,0,0,0-1.25.62l-9,9.22A1.8,1.8,0,0,0,8,19.69,1.78,1.78,0,0,0,8.58,21a1.81,1.81,0,0,0,.57.39,1.48,1.48,0,0,0,.66.1,2,2,0,0,0,1.28-.62l7.12-7.35.15-.16a1.15,1.15,0,0,0,.15-.2.9.9,0,0,0,.12-.24,1.17,1.17,0,0,0,.07-.25.52.52,0,0,0-.05-.27.75.75,0,0,0-.19-.26.73.73,0,0,0-.58-.27,1.29,1.29,0,0,0-.67.38l-5.36,5.53a.5.5,0,0,1-.22.13.46.46,0,0,1-.26,0,.48.48,0,0,1-.22-.12A.41.41,0,0,1,11,17.5a.5.5,0,0,1,.14-.35L16.5,11.6a2.19,2.19,0,0,1,1.29-.67,1.69,1.69,0,0,1,1.37.55,1.54,1.54,0,0,1,.53,1.31,2.26,2.26,0,0,1-.76,1.42L11.8,21.58a3.06,3.06,0,0,1-2,.91H9.71Z" />
+        </svg>
+      );
   }
 
 

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

@@ -53,7 +53,12 @@ module.exports = function(crowi, app) {
       nsSeparator: '::',
     });
 
-  app.use(helmet());
+  app.use(helmet({
+    contentSecurityPolicy: false,
+    expectCt: false,
+    referrerPolicy: false,
+    permittedCrossDomainPolicies: false,
+  }));
 
   app.use((req, res, next) => {
     const now = new Date();

+ 2 - 2
packages/app/src/server/routes/apiv3/healthcheck.js

@@ -6,7 +6,7 @@ const express = require('express');
 
 const router = express.Router();
 
-const helmet = require('helmet');
+const noCache = require('nocache');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 /**
@@ -122,7 +122,7 @@ module.exports = (crowi) => {
    *                  info:
    *                    $ref: '#/components/schemas/HealthcheckInfo'
    */
-  router.get('/', helmet.noCache(), async(req, res) => {
+  router.get('/', noCache(), async(req, res) => {
     let checkServices = req.query.checkServices || [];
     let isStrictly = req.query.strictly != null;
 

+ 2 - 2
packages/app/src/server/routes/apiv3/search.js

@@ -7,7 +7,7 @@ const { body } = require('express-validator');
 
 const router = express.Router();
 
-const helmet = require('helmet');
+const noCache = require('nocache');
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
@@ -41,7 +41,7 @@ module.exports = (crowi) => {
    *                  info:
    *                    type: object
    */
-  router.get('/indices', helmet.noCache(), accessTokenParser, loginRequired, adminRequired, async(req, res) => {
+  router.get('/indices', noCache(), accessTokenParser, loginRequired, adminRequired, async(req, res) => {
     const { searchService } = crowi;
 
     if (!searchService.isConfigured) {

+ 2 - 2
packages/app/src/server/routes/apiv3/statistics.js

@@ -6,7 +6,7 @@ const express = require('express');
 
 const router = express.Router();
 
-const helmet = require('helmet');
+const noCache = require('nocache');
 
 const USER_STATUS_MASTER = {
   1: 'registered',
@@ -97,7 +97,7 @@ module.exports = (crowi) => {
    *                    type: object
    *                    description: Statistics for all user
    */
-  router.get('/user', helmet.noCache(), async(req, res) => {
+  router.get('/user', noCache(), async(req, res) => {
     const data = req.user == null ? await getUserStatisticsForNotLoggedIn() : await getUserStatistics();
     res.status(200).send({ data });
   });

+ 3 - 0
packages/slack/src/index.ts

@@ -26,6 +26,9 @@ export * from './utils/block-kit-builder';
 export * from './utils/check-communicable';
 export * from './utils/get-supported-growi-actions-regexps';
 export * from './utils/post-ephemeral-errors';
+export * from './utils/publish-initial-home-view';
 export * from './utils/reshape-contents-body';
 export * from './utils/slash-command-parser';
 export * from './utils/webclient-factory';
+export * from './utils/welcome-message';
+export * from './utils/required-scopes';

+ 3 - 3
packages/slack/src/utils/check-communicable.ts

@@ -4,6 +4,7 @@ import { WebClient } from '@slack/web-api';
 
 import { generateWebClient } from './webclient-factory';
 import { ConnectionStatus } from '../interfaces/connection-status';
+import { requiredScopes } from './required-scopes';
 
 /**
  * Check whether the HTTP server responds or not.
@@ -45,11 +46,10 @@ const testSlackApiServer = async(client: WebClient): Promise<any> => {
 
 const checkSlackScopes = (resultTestSlackApiServer: any) => {
   const slackScopes = resultTestSlackApiServer.response_metadata.scopes;
-  const correctScopes = ['commands', 'team:read', 'chat:write'];
-  const isPassedScopeCheck = correctScopes.every(e => slackScopes.includes(e));
+  const isPassedScopeCheck = requiredScopes.every(e => slackScopes.includes(e));
 
   if (!isPassedScopeCheck) {
-    throw new Error('The scopes is not appropriate. Required scopes is [\'commands\', \'team:read\', \'chat:write\']');
+    throw new Error(`The scopes you registered are not appropriate. Required scopes are ${requiredScopes}`);
   }
 };
 

+ 28 - 0
packages/slack/src/utils/publish-initial-home-view.ts

@@ -0,0 +1,28 @@
+import { ViewsPublishResponse, WebClient } from '@slack/web-api';
+
+export const publishInitialHomeView = (client: WebClient, userId: string): Promise<ViewsPublishResponse> => {
+  return client.views.publish({
+    user_id: userId,
+    view: {
+      type: 'home',
+      blocks: [
+        {
+          type: 'section',
+          text: {
+            type: 'mrkdwn',
+            text: 'Welcome GROWI Official Bot Home',
+          },
+        },
+        {
+          type: 'section',
+          text: {
+            type: 'mrkdwn',
+            text: 'Learn how to use GROWI Official bot.'
+            // eslint-disable-next-line max-len
+              + 'See <https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/official-bot-settings.html#official-bot-settings | Docs>.',
+          },
+        },
+      ],
+    },
+  });
+};

+ 10 - 0
packages/slack/src/utils/required-scopes.ts

@@ -0,0 +1,10 @@
+export const requiredScopes: string[] = [
+  'commands',
+  'team:read',
+  'chat:write',
+  'channels:join',
+  'channels:history',
+  'groups:history',
+  'im:history',
+  'mpim:history',
+];

+ 4 - 0
packages/slack/src/utils/slash-command-parser.ts

@@ -2,6 +2,10 @@ import { GrowiCommand } from '../interfaces/growi-command';
 import { InvalidGrowiCommandError } from '../models/errors';
 
 export const parseSlashCommand = (slashCommand:{[key:string]:string}): GrowiCommand => {
+  if (slashCommand.text == null) {
+    throw new InvalidGrowiCommandError('The SlashCommand.text is null');
+  }
+
   const trimmedText = slashCommand.text.trim();
   const splitted = trimmedText.split(' ');
 

+ 21 - 0
packages/slack/src/utils/welcome-message.ts

@@ -0,0 +1,21 @@
+import { ChatPostMessageResponse, WebClient } from '@slack/web-api';
+
+export const postWelcomeMessage = (client: WebClient, userId: string): Promise<ChatPostMessageResponse> => {
+  return client.chat.postMessage({
+    channel: userId,
+    user: userId,
+    blocks: [
+      {
+        type: 'section',
+        text: {
+          type: 'mrkdwn',
+          text: ':tada: You have successfully installed GROWI Official bot on this Slack workspace.\n'
+            + 'At first you do `/growi register` in the channel that you want to use.\n'
+            + 'Looking for additional help?'
+            // eslint-disable-next-line max-len
+            + 'See <https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/official-bot-settings.html#official-bot-settings | Docs>.',
+        },
+      },
+    ],
+  });
+};

+ 11 - 0
packages/slackbot-proxy/CHANGES.md

@@ -0,0 +1,11 @@
+# CHANGES
+
+## v1.0.1-RC
+
+* Improvement: Automatically add GROWI bot user to the channel where the command was executed
+* Improvement: Post a welcome message when the installation is successful
+* Improvement: Post a warning message when none of GROWI permit the command
+* Improvement: Permission scopes
+* Improvement: Top page information
+* Improvement: Add a link to docs
+

+ 4 - 1
packages/slackbot-proxy/docker/Dockerfile

@@ -37,7 +37,10 @@ RUN tar cf node_modules.tar node_modules \
 FROM deps-resolver-base AS deps-resolver-prod
 RUN npx lerna bootstrap -- --production
 # make artifacts
-RUN tar cf dependencies.tar node_modules packages/slackbot-proxy/node_modules
+RUN tar cf dependencies.tar
+  node_modules \
+  packages/slack/node_modules \
+  packages/slackbot-proxy/node_modules
 
 
 ##

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "1.0.0-RC",
+  "version": "1.0.1-RC",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -37,6 +37,7 @@
     "bunyan": "^1.8.15",
     "compression": "^1.7.4",
     "cookie-parser": "^1.4.5",
+    "date-fns": "^2.23.0",
     "express-bunyan-logger": "^1.3.3",
     "extensible-custom-error": "^0.0.7",
     "helmet": "^4.6.0",

+ 9 - 1
packages/slackbot-proxy/src/Server.ts

@@ -42,7 +42,12 @@ const connectionOptions: ConnectionOptions = {
 } as ConnectionOptions;
 
 const swaggerSettings = isProduction ? swaggerSettingsForProd : swaggerSettingsForDev;
-const helmetOptions = isProduction ? {} : {
+const helmetOptions = isProduction ? {
+  contentSecurityPolicy: false,
+  expectCt: false,
+  referrerPolicy: false,
+  permittedCrossDomainPolicies: false,
+} : {
   contentSecurityPolicy: {
     directives: {
       defaultSrc: ['\'self\''],
@@ -51,6 +56,9 @@ const helmetOptions = isProduction ? {} : {
       scriptSrc: ['\'self\'', 'https: \'unsafe-inline\''],
     },
   },
+  expectCt: false,
+  referrerPolicy: false,
+  permittedCrossDomainPolicies: false,
 };
 
 @Configuration({

+ 108 - 61
packages/slackbot-proxy/src/controllers/slack.ts

@@ -1,13 +1,16 @@
 import {
-  BodyParams, Controller, Get, Inject, Post, Req, Res, UseBefore,
+  BodyParams, Controller, Get, Inject, PlatformResponse, Post, Req, Res, UseBefore,
 } from '@tsed/common';
 
 import axios from 'axios';
 
 import { WebAPICallResult } from '@slack/web-api';
+import { Installation } from '@slack/oauth';
+
 
 import {
   markdownSectionBlock, GrowiCommand, parseSlashCommand, postEphemeralErrors, verifySlackRequest, generateWebClient,
+  InvalidGrowiCommandError, requiredScopes, postWelcomeMessage, publishInitialHomeView,
 } from '@growi/slack';
 
 import { Relation } from '~/entities/relation';
@@ -25,6 +28,7 @@ import { RelationsService } from '~/services/RelationsService';
 import { UnregisterService } from '~/services/UnregisterService';
 import { InvalidUrlError } from '../models/errors';
 import loggerFactory from '~/utils/logger';
+import { JoinToConversationMiddleware } from '~/middlewares/slack-to-growi/join-to-conversation';
 
 
 const logger = loggerFactory('slackbot-proxy:controllers:slack');
@@ -97,15 +101,27 @@ export class SlackCtrl {
   }
 
   @Post('/commands')
-  @UseBefore(AddSigningSecretToReq, verifySlackRequest, AuthorizeCommandMiddleware)
+  @UseBefore(AddSigningSecretToReq, verifySlackRequest, AuthorizeCommandMiddleware, JoinToConversationMiddleware)
   async handleCommand(@Req() req: SlackOauthReq, @Res() res: Res): Promise<void|string|Res|WebAPICallResult> {
     const { body, authorizeResult } = req;
 
-    if (body.text == null) {
-      return 'No text.';
-    }
+    let growiCommand;
 
-    const growiCommand = parseSlashCommand(body);
+    try {
+      growiCommand = parseSlashCommand(body);
+    }
+    catch (err) {
+      if (err instanceof InvalidGrowiCommandError) {
+        res.json({
+          blocks: [
+            markdownSectionBlock('*Command type is not specified.*'),
+            markdownSectionBlock('Run `/growi help` to check the commands you can use.'),
+          ],
+        });
+      }
+      logger.error(err.message);
+      return;
+    }
 
     // register
     if (growiCommand.growiCommandType === 'register') {
@@ -165,50 +181,63 @@ export class SlackCtrl {
 
     const baseDate = new Date();
 
-    const relationsForSingleUse:Relation[] = [];
+    const allowedRelationsForSingleUse:Relation[] = [];
+    const disallowedGrowiUrls: Set<string> = new Set();
+
+    // check permission for single use
     await Promise.all(relations.map(async(relation) => {
       const isSupported = await this.relationsService.isSupportedGrowiCommandForSingleUse(relation, growiCommand.growiCommandType, baseDate);
       if (isSupported) {
-        relationsForSingleUse.push(relation);
+        allowedRelationsForSingleUse.push(relation);
+      }
+      else {
+        disallowedGrowiUrls.add(relation.growiUri);
       }
     }));
 
-    let isCommandPermitted = false;
-
-    if (relationsForSingleUse.length > 0) {
-      isCommandPermitted = true;
-      body.growiUrisForSingleUse = relationsForSingleUse.map(v => v.growiUri);
+    // select GROWI
+    if (allowedRelationsForSingleUse.length > 0) {
+      body.growiUrisForSingleUse = allowedRelationsForSingleUse.map(v => v.growiUri);
       return this.selectGrowiService.process(growiCommand, authorizeResult, body);
     }
 
+    // check permission for broadcast use
     const relationsForBroadcastUse:Relation[] = [];
     await Promise.all(relations.map(async(relation) => {
       const isSupported = await this.relationsService.isSupportedGrowiCommandForBroadcastUse(relation, growiCommand.growiCommandType, baseDate);
       if (isSupported) {
         relationsForBroadcastUse.push(relation);
       }
+      else {
+        disallowedGrowiUrls.add(relation.growiUri);
+      }
     }));
 
-    /*
-     * forward to GROWI server
-     */
+    // forward to GROWI server
     if (relationsForBroadcastUse.length > 0) {
-      isCommandPermitted = true;
       this.sendCommand(growiCommand, relationsForBroadcastUse, body);
     }
 
-    if (!isCommandPermitted) {
-      const botToken = relations[0].installation?.data.bot?.token;
-
+    // when all of GROWI disallowed
+    if (relations.length === disallowedGrowiUrls.size) {
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      const client = generateWebClient(botToken!);
+      const client = generateWebClient(authorizeResult.botToken!);
+
+      const linkUrlList = Array.from(disallowedGrowiUrls).map((growiUrl) => {
+        return '\n'
+          + `• ${new URL('/admin/slack-integration', growiUrl).toString()}`;
+      });
 
       return client.chat.postEphemeral({
         text: 'Error occured.',
         channel: body.channel_id,
         user: body.user_id,
         blocks: [
-          markdownSectionBlock(`It is not allowed to run *'${growiCommand.growiCommandType}'* command to this GROWI.`),
+          markdownSectionBlock('*None of GROWI permitted the command.*'),
+          markdownSectionBlock(`*'${growiCommand.growiCommandType}'* command was not allowed.`),
+          markdownSectionBlock(
+            `To use this command, modify settings from following pages: ${linkUrlList}`,
+          ),
         ],
       });
     }
@@ -307,50 +336,68 @@ export class SlackCtrl {
   }
 
   @Get('/oauth_redirect')
-  async handleOauthRedirect(@Req() req: Req, @Res() res: Res): Promise<void> {
-
-    if (req.query.state === '') {
-      res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
-      res.end('<html>'
-      + '<head><meta name="viewport" content="width=device-width,initial-scale=1"></head>'
-      + '<body style="text-align:center; padding-top:20%;">'
-      + '<h1>Illegal state, try it again.</h1>'
-      + '<a href="/">'
-      + 'Go to install page'
-      + '</a>'
-      + '</body></html>');
+  async handleOauthRedirect(@Req() req: Req, @Res() serverRes: Res, @Res() platformRes: PlatformResponse): Promise<void|string> {
+
+    // create 'Add to Slack' url
+    const addToSlackUrl = await this.installerService.installer.generateInstallUrl({
+      scopes: requiredScopes,
+    });
+
+    const state = req.query.state;
+    if (state == null || state === '') {
+      return platformRes.status(400).render('install-failed.ejs', { url: addToSlackUrl });
     }
 
-    await this.installerService.installer.handleCallback(req, res, {
-      success: (installation, metadata, req, res) => {
-        logger.info('Success to install', { installation, metadata });
+    // promisify
+    const installPromise = new Promise<Installation>((resolve, reject) => {
+      this.installerService.installer.handleCallback(req, serverRes, {
+        success: async(installation, metadata) => {
+          logger.info('Success to install', { installation, metadata });
+          resolve(installation);
+        },
+        failure: async(error) => {
+          reject(error); // go to catch block
+        },
+      });
+    });
+
+    let httpStatus = 200;
+    let httpBody;
+    try {
+      const installation = await installPromise;
 
+      // check whether bot is not null
+      if (installation.bot == null) {
+        logger.warn('Success to install but something wrong. `installation.bot` is null.');
+        httpStatus = 500;
+        httpBody = await platformRes.render('install-succeeded-but-has-problem.ejs', { reason: '`installation.bot` is null' });
+      }
+      // MAIN PATH: everything is fine
+      else {
         const appPageUrl = `https://slack.com/apps/${installation.appId}`;
+        httpBody = await platformRes.render('install-succeeded.ejs', { appPageUrl });
 
-        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
-        res.end('<html>'
-        + '<head><meta name="viewport" content="width=device-width,initial-scale=1"></head>'
-        + '<body style="text-align:center; padding-top:20%;">'
-        + '<h1>Congratulations!</h1>'
-        + '<p>GROWI Bot installation has succeeded.</p>'
-        + `<a href="${appPageUrl}">`
-        + 'Access to Slack App detail page.'
-        + '</a>'
-        + '</body></html>');
-      },
-      failure: (error, installOptions, req, res) => {
-        res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
-        res.end('<html>'
-        + '<head><meta name="viewport" content="width=device-width,initial-scale=1"></head>'
-        + '<body style="text-align:center; padding-top:20%;">'
-        + '<h1>GROWI Bot installation failed</h1>'
-        + '<p>Please contact administrators of your workspace</p>'
-        + 'Reference: <a href="https://slack.com/help/articles/222386767-Manage-app-installation-settings-for-your-workspace">'
-        + 'Manage app installation settings for your workspace'
-        + '</a>'
-        + '</body></html>');
-      },
-    });
+        // generate client
+        const client = generateWebClient(installation.bot.token);
+
+        const userId = installation.user.id;
+
+        await Promise.all([
+          // post message
+          postWelcomeMessage(client, userId),
+          // publish home
+          publishInitialHomeView(client, userId),
+        ]);
+      }
+    }
+    catch (error) {
+      logger.error(error);
+      httpStatus = 500;
+      httpBody = await platformRes.status(400).render('install-failed.ejs', { url: addToSlackUrl });
+    }
+
+    platformRes.status(httpStatus);
+    return httpBody;
   }
 
 }

+ 2 - 9
packages/slackbot-proxy/src/controllers/top.ts

@@ -2,6 +2,7 @@ import {
   Controller, Get, Inject, View,
 } from '@tsed/common';
 
+import { requiredScopes } from '@growi/slack';
 import { InstallerService } from '~/services/InstallerService';
 
 const isOfficialMode = process.env.OFFICIAL_MODE === 'true';
@@ -18,15 +19,7 @@ export class TopCtrl {
   async getTopPage(): Promise<any> {
     const url = await this.installerService.installer.generateInstallUrl({
       // Add the scopes your app needs
-      scopes: [
-        'channels:history',
-        'commands',
-        'groups:history',
-        'im:history',
-        'mpim:history',
-        'chat:write',
-        'team:read',
-      ],
+      scopes: requiredScopes,
     });
 
     return { url, isOfficialMode };

+ 32 - 0
packages/slackbot-proxy/src/middlewares/slack-to-growi/join-to-conversation.ts

@@ -0,0 +1,32 @@
+import { generateWebClient } from '@growi/slack';
+import {
+  IMiddleware, Middleware, Req,
+} from '@tsed/common';
+
+import Logger from 'bunyan';
+import { SlackOauthReq } from '~/interfaces/slack-to-growi/slack-oauth-req';
+
+import loggerFactory from '~/utils/logger';
+
+const logger: Logger = loggerFactory('slackbot-proxy:middlewares:JoinToConversationsMiddleware');
+
+
+/**
+ * This middleware should be processed after AuthorizeCommandMiddleware or AuthorizeInteractionMiddleware
+ */
+@Middleware()
+export class JoinToConversationMiddleware implements IMiddleware {
+
+  async use(@Req() req: SlackOauthReq): Promise<void> {
+    const { body, authorizeResult } = req;
+
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const client = generateWebClient(authorizeResult.botToken!);
+
+    const joinResult = await client.conversations.join({ channel: body.channel_id });
+    if (!joinResult.ok) {
+      logger.error(joinResult.error, joinResult);
+    }
+  }
+
+}

+ 21 - 0
packages/slackbot-proxy/src/views/install-failed.ejs

@@ -0,0 +1,21 @@
+<%- include('commons/head'); %>
+
+<body>
+  <div class="container">
+    <div class="row">
+      <div class="col text-center">
+        <h1 class="my-5">GROWI Bot installation failed..</h1>
+        <p>
+          Retry from
+          <a href=<%- url %>>
+            <img alt="Add to Slack" height="40" width="139" src="/images/add-to-slack.png"/>
+          </a>
+        </p>
+        <p>
+          Or, please contact administrators of your workspace<br>
+          Reference: <a href="https://slack.com/help/articles/222386767-Manage-app-installation-settings-for-your-workspace">Manage app installation settings for your workspace</a>
+        </p>
+      </div>
+    </div>
+  </div>
+</body>

+ 18 - 0
packages/slackbot-proxy/src/views/install-succeeded-but-has-problem.ejs

@@ -0,0 +1,18 @@
+<%- include('commons/head'); %>
+
+<body>
+  <div class="container">
+    <div class="row">
+      <div class="col text-center">
+        <h1 class="my-5">GROWI Bot installation has succeeded, but something went wrong..</h1>
+        <p>
+          Reason: <%- reason%>
+        </p>
+        <p>
+          Please contact administrators of your workspace<br>
+          Reference: <a href="https://slack.com/help/articles/222386767-Manage-app-installation-settings-for-your-workspace">Manage app installation settings for your workspace</a>
+        </p>
+      </div>
+    </div>
+  </div>
+</body>

+ 21 - 0
packages/slackbot-proxy/src/views/install-succeeded.ejs

@@ -0,0 +1,21 @@
+<%- include('commons/head'); %>
+
+<body>
+  <div class="container">
+    <div class="row">
+      <div class="col text-center">
+        <h1 class="my-5">Congratulations!</h1>
+        <h2 class="my-5">GROWI Bot installation has succeeded!</h2>
+        <p>
+          Retry from
+          <a href=<%- appPageUrl %>>
+            Access to Slack App detail page.
+          </a>
+        </p>
+        <p>
+          <a class="btn btn-outline-success" href="https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/official-bot-settings.html">Getting started</a>
+        </p>
+      </div>
+    </div>
+  </div>
+</body>

+ 1 - 0
packages/slackbot-proxy/src/views/top.ejs

@@ -14,6 +14,7 @@
           <img alt="Add to Slack" height="40" width="139" src="/images/add-to-slack.png"/>
         </a>
       </div>
+      <a class="btn btn-outline-success" href="https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/official-bot-settings.html">Getting started</a>
       <div class="d-flex justify-content-evenly my-3">
         <% if (isOfficialMode) { %>
           <a href="/privacy">

+ 4 - 0
packages/slackbot-proxy/tsconfig.base.json

@@ -2,10 +2,14 @@
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
   },
+  "include": [
+    "src"
+  ],
   "exclude": [
     "node_modules",
     "config",
     "dist",
+    "src/public/**",
     "**/*.test.ts"
   ]
 }

+ 9 - 96
yarn.lock

@@ -4970,7 +4970,7 @@ camelcase@^6.2.0:
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
   integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
 
-camelize@1.0.0, camelize@^1.0.0:
+camelize@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b"
 
@@ -5835,10 +5835,6 @@ content-disposition@0.5.3:
   dependencies:
     safe-buffer "5.1.2"
 
-content-security-policy-builder@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/content-security-policy-builder/-/content-security-policy-builder-2.0.0.tgz#8749a1d542fcbe82237281ea9f716ce68b394dd2"
-
 content-type@~1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
@@ -6475,10 +6471,6 @@ dashdash@^1.12.0, dashdash@^1.14.0:
   dependencies:
     assert-plus "^1.0.0"
 
-dasherize@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/dasherize/-/dasherize-2.0.0.tgz#6d809c9cd0cf7bb8952d80fc84fa13d47ddb1308"
-
 data-urls@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
@@ -6493,15 +6485,10 @@ date-and-time@^1.0.0:
   resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-1.0.0.tgz#0062394bdf6f44e961f0db00511cb19cdf3cc0a5"
   integrity sha512-477D7ypIiqlXBkxhU7YtG9wWZJEQ+RUpujt2quTfgf4+E8g5fNUkB0QIL0bVyP5/TKBg8y55Hfa1R/c4bt3dEw==
 
-date-fns@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0.tgz#52f05c6ae1fe0e395670082c72b690ab781682d0"
-  integrity sha512-nGZDA64Ktq5uTWV4LEH3qX+foV4AguT5qxwRlJDzJtf57d4xLNwtwrfb7SzKCoikoae8Bvxf0zdaEG/xWssp/w==
-
-date-fns@^2.19.0:
-  version "2.22.1"
-  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.22.1.tgz#1e5af959831ebb1d82992bf67b765052d8f0efc4"
-  integrity sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==
+date-fns@^2.19.0, date-fns@^2.23.0:
+  version "2.23.0"
+  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.23.0.tgz#4e886c941659af0cf7b30fafdd1eaa37e88788a9"
+  integrity sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==
 
 date-format@^2.0.0:
   version "2.1.0"
@@ -6830,10 +6817,6 @@ dir-glob@^3.0.1:
   dependencies:
     path-type "^4.0.0"
 
-dns-prefetch-control@0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz#60ddb457774e178f1f9415f0cabb0e85b0b300b2"
-
 doctrine@3.0.0, doctrine@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
@@ -6910,10 +6893,6 @@ domutils@^1.5.1:
     dom-serializer "0"
     domelementtype "1"
 
-dont-sniff-mimetype@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz#5932890dc9f4e2f19e5eb02a20026e5e5efc8f58"
-
 dot-case@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
@@ -7952,10 +7931,6 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
   dependencies:
     homedir-polyfill "^1.0.1"
 
-expect-ct@0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/expect-ct/-/expect-ct-0.1.1.tgz#de84476a2dbcb85000d5903737e9bc8a5ba7b897"
-
 expect@^27.0.6:
   version "27.0.6"
   resolved "https://registry.yarnpkg.com/expect/-/expect-27.0.6.tgz#a4d74fbe27222c718fff68ef49d78e26a8fd4c05"
@@ -8654,10 +8629,6 @@ fragment-cache@^0.2.1:
   dependencies:
     map-cache "^0.2.2"
 
-frameguard@3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/frameguard/-/frameguard-3.0.0.tgz#7bcad469ee7b96e91d12ceb3959c78235a9272e9"
-
 fresh@0.5.2, fresh@^0.5.2:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
@@ -9585,37 +9556,6 @@ header-case@^2.0.4:
     capital-case "^1.0.4"
     tslib "^2.0.3"
 
-helmet-crossdomain@0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/helmet-crossdomain/-/helmet-crossdomain-0.3.0.tgz#707e2df930f13ad61f76ed08e1bb51ab2b2e85fa"
-
-helmet-csp@2.7.1:
-  version "2.7.1"
-  resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.7.1.tgz#e8e0b5186ffd4db625cfcce523758adbfadb9dca"
-  dependencies:
-    camelize "1.0.0"
-    content-security-policy-builder "2.0.0"
-    dasherize "2.0.0"
-    platform "1.3.5"
-
-helmet@^3.13.0:
-  version "3.13.0"
-  resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.13.0.tgz#d6d46763538f77b437be77f06d0af42078b2c656"
-  dependencies:
-    dns-prefetch-control "0.1.0"
-    dont-sniff-mimetype "1.0.0"
-    expect-ct "0.1.1"
-    frameguard "3.0.0"
-    helmet-crossdomain "0.3.0"
-    helmet-csp "2.7.1"
-    hide-powered-by "1.0.0"
-    hpkp "2.0.0"
-    hsts "2.1.0"
-    ienoopen "1.0.0"
-    nocache "2.0.0"
-    referrer-policy "1.1.0"
-    x-xss-protection "1.1.0"
-
 helmet@^4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.6.0.tgz#579971196ba93c5978eb019e4e8ec0e50076b4df"
@@ -9625,10 +9565,6 @@ hex-color-regex@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
 
-hide-powered-by@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.0.0.tgz#4a85ad65881f62857fc70af7174a1184dccce32b"
-
 highlight.js@9.18.1:
   version "9.18.1"
   resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.1.tgz#ed21aa001fe6252bb10a3d76d47573c6539fe13c"
@@ -9684,10 +9620,6 @@ hosted-git-info@^4.0.1:
   dependencies:
     lru-cache "^6.0.0"
 
-hpkp@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/hpkp/-/hpkp-2.0.0.tgz#10e142264e76215a5d30c44ec43de64dee6d1672"
-
 hsl-regex@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e"
@@ -9696,10 +9628,6 @@ hsla-regex@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
 
-hsts@2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.1.0.tgz#cbd6c918a2385fee1dd5680bfb2b3a194c0121cc"
-
 html-comment-regex@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
@@ -9934,10 +9862,6 @@ ieee754@^1.1.4:
   version "1.1.8"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
 
-ienoopen@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/ienoopen/-/ienoopen-1.0.0.tgz#346a428f474aac8f50cf3784ea2d0f16f62bda6b"
-
 iferr@^0.1.5:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
@@ -13297,9 +13221,10 @@ no-case@^3.0.4:
     lower-case "^2.0.2"
     tslib "^2.0.3"
 
-nocache@2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.0.0.tgz#202b48021a0c4cbde2df80de15a17443c8b43980"
+nocache@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/nocache/-/nocache-3.0.1.tgz#54d8b53a7e0a0aa1a288cfceab8a3cefbcde67d4"
+  integrity sha512-Gh39xwJwBKy0OvFmWfBs/vDO4Nl7JhnJtkqNP76OUinQz7BiMoszHYrIDHHAaqVl/QKVxCEy4ZxC/XZninu7nQ==
 
 node-dev@^4.0.0:
   version "4.0.0"
@@ -14913,10 +14838,6 @@ plantuml-encoder@^1.2.5:
     pako "1.0.3"
     utf8-bytes "0.0.1"
 
-platform@1.3.5:
-  version "1.3.5"
-  resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.5.tgz#fb6958c696e07e2918d2eeda0f0bc9448d733444"
-
 pluralize@^8.0.0:
   version "8.0.0"
   resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
@@ -16597,10 +16518,6 @@ redux@^4.0.4:
     loose-envify "^1.4.0"
     symbol-observable "^1.2.0"
 
-referrer-policy@1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.1.0.tgz#35774eb735bf50fb6c078e83334b472350207d79"
-
 reflect-metadata@^0.1.13:
   version "0.1.13"
   resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
@@ -20638,10 +20555,6 @@ x-is-string@^0.1.0:
   resolved "https://registry.yarnpkg.com/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82"
   integrity sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=
 
-x-xss-protection@1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/x-xss-protection/-/x-xss-protection-1.1.0.tgz#4f1898c332deb1e7f2be1280efb3e2c53d69c1a7"
-
 xdg-basedir@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"