Преглед изворни кода

Merge pull request #4388 from weseek/master

Release v4.4.7
Yuki Takei пре 4 година
родитељ
комит
4d3db6f99a
100 измењених фајлова са 735 додато и 643 уклоњено
  1. 9 9
      .devcontainer/Dockerfile
  2. 1 0
      .gitattributes
  3. 8 0
      .github/workflows/ci.yml
  4. 2 0
      .github/workflows/release-rc.yml
  5. 1 1
      .github/workflows/release-slackbot-proxy.yml
  6. 6 3
      .github/workflows/release.yml
  7. 11 0
      THIRD-PARTY-NOTICES.md
  8. 1 1
      lerna.json
  9. 9 6
      package.json
  10. 2 0
      packages/app/.env.development
  11. 1 0
      packages/app/.env.production
  12. 2 1
      packages/app/.eslintrc.js
  13. 1 1
      packages/app/.gitignore
  14. 1 12
      packages/app/bin/cdn/cdn-resources-downloader.ts
  15. 0 1
      packages/app/config/cdn.js
  16. 2 2
      packages/app/config/webpack.prod.js
  17. 6 11
      packages/app/docker/Dockerfile
  18. 6 0
      packages/app/docker/Dockerfile.dockerignore
  19. 1 1
      packages/app/docker/docker-entrypoint.sh
  20. 6 0
      packages/app/docker/nocdn/.env.production.local
  21. 0 5
      packages/app/docker/nocdn/env.prod.js
  22. 21 5
      packages/app/jest.config.js
  23. 8 5
      packages/app/migrate-mongo-config.js
  24. 16 13
      packages/app/package.json
  25. 3 0
      packages/app/public/static/dict/base.dat.gz
  26. 3 0
      packages/app/public/static/dict/cc.dat.gz
  27. 3 0
      packages/app/public/static/dict/check.dat.gz
  28. 3 0
      packages/app/public/static/dict/tid.dat.gz
  29. 3 0
      packages/app/public/static/dict/tid_map.dat.gz
  30. 3 0
      packages/app/public/static/dict/tid_pos.dat.gz
  31. 3 0
      packages/app/public/static/dict/unk.dat.gz
  32. 3 0
      packages/app/public/static/dict/unk_char.dat.gz
  33. 3 0
      packages/app/public/static/dict/unk_compat.dat.gz
  34. 3 0
      packages/app/public/static/dict/unk_invoke.dat.gz
  35. 3 0
      packages/app/public/static/dict/unk_map.dat.gz
  36. 3 0
      packages/app/public/static/dict/unk_pos.dat.gz
  37. 0 86
      packages/app/resource/cdn-manifests.js
  38. 1 1
      packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  39. 4 4
      packages/app/src/components/Me/EditorSettings.tsx
  40. 2 5
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  41. 1 1
      packages/app/src/components/Sidebar/RecentChanges.jsx
  42. 2 3
      packages/app/src/migrations/20180926134048-make-email-unique.js
  43. 3 3
      packages/app/src/migrations/20180927102719-init-serverurl.js
  44. 3 4
      packages/app/src/migrations/20181019114028-abolish-page-group-relation.js
  45. 2 2
      packages/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js
  46. 3 4
      packages/app/src/migrations/20190618104011-add-config-app-installed.js
  47. 2 2
      packages/app/src/migrations/20190619055421-adjust-page-grant.js
  48. 2 2
      packages/app/src/migrations/20190624110950-fill-last-update-user.js
  49. 2 2
      packages/app/src/migrations/20190629193445-make-root-page-public.js
  50. 2 2
      packages/app/src/migrations/20191102223900-drop-configs-indices.js
  51. 2 2
      packages/app/src/migrations/20191102223901-drop-pages-indices.js
  52. 3 3
      packages/app/src/migrations/20191126173016-adjust-pages-path.js
  53. 2 2
      packages/app/src/migrations/20191127023815-drop-wrong-index-of-page-tag-relation.js
  54. 2 3
      packages/app/src/migrations/20200402160380-remove-deleteduser-from-relationgroup.js
  55. 2 2
      packages/app/src/migrations/20200420160390-remove-crowi-layout.js
  56. 3 3
      packages/app/src/migrations/20200512005851-remove-behavior-type.js
  57. 2 2
      packages/app/src/migrations/20200514001356-update-theme-color-for-dark.js
  58. 2 3
      packages/app/src/migrations/20200620203632-normalize-locale-id.js
  59. 3 3
      packages/app/src/migrations/20200827045151-remove-layout-setting.js
  60. 3 3
      packages/app/src/migrations/20200828024025-copy-aws-setting.js
  61. 3 3
      packages/app/src/migrations/20200901034313-update-mail-transmission.js
  62. 2 2
      packages/app/src/migrations/20200901034314-update-mail-transmission-fix.js
  63. 3 3
      packages/app/src/migrations/20200903080025-remove-timeline-type.js.js
  64. 3 3
      packages/app/src/migrations/20200915035234-rename-s3-config.js
  65. 2 3
      packages/app/src/migrations/20210420160380-convert-double-to-date.js
  66. 35 44
      packages/app/src/migrations/20210830074539-update-configs-for-slackbot.js
  67. 3 4
      packages/app/src/migrations/20210906194521-slack-app-integration-set-default-value.js
  68. 57 43
      packages/app/src/migrations/20210913153942-migrate-slack-app-integration-schema.js
  69. 1 1
      packages/app/src/server/console.js
  70. 28 5
      packages/app/src/server/crowi/index.js
  71. 1 1
      packages/app/src/server/models/config.ts
  72. 1 1
      packages/app/src/server/models/editor-settings.ts
  73. 1 1
      packages/app/src/server/models/password-reset-order.ts
  74. 1 1
      packages/app/src/server/models/update-post.ts
  75. 2 3
      packages/app/src/server/routes/apiv3/slack-integration-settings.js
  76. 151 244
      packages/app/src/server/service/slack-command-handler/search.js
  77. 28 6
      packages/app/src/server/service/slack-integration.ts
  78. 5 2
      packages/app/src/server/service/socket-io.js
  79. 0 40
      packages/app/src/test/config/migrate.test.js
  80. 0 0
      packages/app/src/test/integration/crowi/crowi.test.js
  81. 1 1
      packages/app/src/test/integration/global-setup.js
  82. 0 0
      packages/app/src/test/integration/global-teardown.js
  83. 0 0
      packages/app/src/test/integration/middlewares/access-token-parser.test.js
  84. 0 0
      packages/app/src/test/integration/middlewares/login-required.test.js
  85. 122 0
      packages/app/src/test/integration/migrations/20210913153942-migrate-slack-app-integration-schema.test.ts
  86. 0 0
      packages/app/src/test/integration/models/config.test.js
  87. 0 0
      packages/app/src/test/integration/models/page.test.js
  88. 0 0
      packages/app/src/test/integration/models/share-link.test.js
  89. 0 0
      packages/app/src/test/integration/models/update-post.test.js
  90. 0 0
      packages/app/src/test/integration/models/user.test.js
  91. 0 0
      packages/app/src/test/integration/service/acl.test.js
  92. 0 0
      packages/app/src/test/integration/service/config-manager.test.js
  93. 2 2
      packages/app/src/test/integration/service/page.test.js
  94. 0 0
      packages/app/src/test/integration/service/passport.test.js
  95. 0 0
      packages/app/src/test/integration/service/search-delegator/searchbox.test.js
  96. 1 5
      packages/app/src/test/integration/setup-crowi.js
  97. 1 1
      packages/app/src/test/integration/setup.js
  98. 0 0
      packages/app/src/test/integration/utils/slack-legacy.test.js
  99. 0 0
      packages/app/src/test/unit/middlewares/safe-redirect.test.js
  100. 71 0
      packages/app/src/test/unit/migrate-mongo-config.test.js

+ 9 - 9
.devcontainer/Dockerfile

@@ -29,15 +29,15 @@ RUN chown -R $USER_UID:$USER_GID /home/$USERNAME /workspace;
 # * any needed dependencies after executing "apt-get update". *
 # * any needed dependencies after executing "apt-get update". *
 # * See https://docs.docker.com/engine/reference/builder/#run *
 # * See https://docs.docker.com/engine/reference/builder/#run *
 # *************************************************************
 # *************************************************************
-# ENV DEBIAN_FRONTEND=noninteractive
-# RUN apt-get update \
-#    && apt-get -y install --no-install-recommends <your-package-list-here> \
-#    #
-#    # Clean up
-#    && apt-get autoremove -y \
-#    && apt-get clean -y \
-#    && rm -rf /var/lib/apt/lists/*
-# ENV DEBIAN_FRONTEND=dialog
+ENV DEBIAN_FRONTEND=noninteractive
+RUN apt-get update \
+   && apt-get -y install --no-install-recommends git-lfs \
+
+   # Clean up
+   && apt-get autoremove -y \
+   && apt-get clean -y \
+   && rm -rf /var/lib/apt/lists/*
+ENV DEBIAN_FRONTEND=dialog
 
 
 # Uncomment to default to non-root user
 # Uncomment to default to non-root user
 # USER $USER_UID
 # USER $USER_UID

+ 1 - 0
.gitattributes

@@ -0,0 +1 @@
+*.gz filter=lfs diff=lfs merge=lfs -text

+ 8 - 0
.github/workflows/ci.yml

@@ -208,6 +208,8 @@ jobs:
     - name: Build
     - name: Build
       run: |
       run: |
         yarn lerna run build
         yarn lerna run build
+      env:
+        ANALYZE_BUNDLE_SIZE: ${{ matrix.node-version == '14.x' }}
     - name: lerna bootstrap --production
     - name: lerna bootstrap --production
       run: |
       run: |
         npx lerna bootstrap -- --production
         npx lerna bootstrap -- --production
@@ -235,6 +237,12 @@ jobs:
       env:
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
 
 
+    - name: Upload report as artifact
+      uses: actions/upload-artifact@v2
+      with:
+        name: Bundle Analyzing Report
+        path: packages/app/report/bundle-analyzer.html
+
     - name: Slack Notification
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
       uses: weseek/ghaction-slack-notification@master
       if: failure()
       if: failure()

+ 2 - 0
.github/workflows/release-rc.yml

@@ -13,6 +13,8 @@ jobs:
 
 
     steps:
     steps:
     - uses: actions/checkout@v2
     - uses: actions/checkout@v2
+      with:
+        lfs: true
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@1.1.0
       uses: myrotvorets/info-from-package-json-action@1.1.0

+ 1 - 1
.github/workflows/release-slackbot-proxy.yml

@@ -118,7 +118,7 @@ jobs:
 
 
     - name: Bump versions for next RC
     - name: Bump versions for next RC
       run: |
       run: |
-        node ./bin/github-actions/bump-versions -i prerelease -d packages/slackbot-proxy --preid slackbot-proxy --update-dependencies false
+        yarn bump-versions:slackbot-proxy
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@1.1.0
       uses: myrotvorets/info-from-package-json-action@1.1.0

+ 6 - 3
.github/workflows/release.yml

@@ -34,7 +34,7 @@ jobs:
 
 
     - name: Bump versions
     - name: Bump versions
       run: |
       run: |
-        node ./bin/github-actions/bump-versions -i patch
+        yarn bump-versions:patch
         sh ./packages/app/bin/github-actions/update-readme.sh
         sh ./packages/app/bin/github-actions/update-readme.sh
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
@@ -95,8 +95,8 @@ jobs:
 
 
     - name: Bump versions for next RC
     - name: Bump versions for next RC
       run: |
       run: |
-        node ./bin/github-actions/bump-versions -i prerelease
-        node ./bin/github-actions/bump-versions -i prerelease -d packages/slackbot-proxy --preid slackbot-proxy --update-dependencies false
+        yarn bump-versions:rc
+        yarn bump-versions:slackbot-proxy
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
       uses: myrotvorets/info-from-package-json-action@1.1.0
       uses: myrotvorets/info-from-package-json-action@1.1.0
@@ -134,6 +134,7 @@ jobs:
     - uses: actions/checkout@v2
     - uses: actions/checkout@v2
       with:
       with:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
+        lfs: true
 
 
     - name: Setup suffix
     - name: Setup suffix
       id: suffix
       id: suffix
@@ -184,6 +185,8 @@ jobs:
         file: ./packages/app/docker/Dockerfile
         file: ./packages/app/docker/Dockerfile
         platforms: linux/amd64
         platforms: linux/amd64
         push: true
         push: true
+        build-args: |
+          flavor=${{ matrix.flavor }}
         cache-from: type=local,src=/tmp/.buildx-cache
         cache-from: type=local,src=/tmp/.buildx-cache
         cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
         cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
         tags: ${{ steps.meta.outputs.tags }}
         tags: ${{ steps.meta.outputs.tags }}

+ 11 - 0
THIRD-PARTY-NOTICES.md

@@ -17,6 +17,7 @@ https://github.com/weseek/growi.
 3. Microsoft/vscode (https://github.com/Microsoft/vscode)
 3. Microsoft/vscode (https://github.com/Microsoft/vscode)
 4. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
 4. stephenhutchings/typicons.font (https://github.com/stephenhutchings/typicons.font)
 5. EmojiOne Version 3 (https://github.com/joypixels/emojione/tree/v3.1.1)
 5. EmojiOne Version 3 (https://github.com/joypixels/emojione/tree/v3.1.1)
+6. Kuromoji.js (https://github.com/takuyaa/kuromoji.js)
 
 
 
 
 License Notice for Apache License, Version 2.0 Derivative Works
 License Notice for Apache License, Version 2.0 Derivative Works
@@ -108,3 +109,13 @@ https://creativecommons.org/licenses/by/4.0/
 ```
 ```
 author: "EmojiOne <ryan@emojione.com> (http://emojione.com)"
 author: "EmojiOne <ryan@emojione.com> (http://emojione.com)"
 ```
 ```
+
+
+License Notice for Kuromoji.js
+------------------------
+
+https://github.com/takuyaa/kuromoji.js/blob/master/LICENSE-2.0.txt
+
+```
+author: "Takuya Asano <takuya.a@gmail.com>"
+```

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
 {
   "npmClient": "yarn",
   "npmClient": "yarn",
   "useWorkspaces": true,
   "useWorkspaces": true,
-  "version": "4.4.6",
+  "version": "4.4.7-RC.0",
   "packages": [
   "packages": [
     "packages/*"
     "packages/*"
   ]
   ]

+ 9 - 6
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "4.4.6",
+  "version": "4.4.7-RC.0",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -35,6 +35,9 @@
     "app:server": "yarn lerna run server --scope @growi/app",
     "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:build": "yarn lerna run build --scope @growi/slackbot-proxy --scope @growi/slack",
     "slackbot-proxy:server": "yarn lerna run start:prod --scope @growi/slackbot-proxy",
     "slackbot-proxy:server": "yarn lerna run start:prod --scope @growi/slackbot-proxy",
+    "bump-versions:patch": "node ./bin/github-actions/bump-versions -i patch",
+    "bump-versions:rc": "node ./bin/github-actions/bump-versions -i prerelease",
+    "bump-versions:slackbot-proxy": "node ./bin/github-actions/bump-versions -i prerelease -d packages/slackbot-proxy --preid slackbot-proxy --update-dependencies false",
     "//// scripts for backward compatibility": "",
     "//// scripts for backward compatibility": "",
     "build:prod": "echo !!! CAUTION !!! ==> The script 'build:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:build",
     "build:prod": "echo !!! CAUTION !!! ==> The script 'build:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:build",
     "server:prod": "echo !!! CAUTION !!! ==> The script 'server:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:server"
     "server:prod": "echo !!! CAUTION !!! ==> The script 'server:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:server"
@@ -43,10 +46,7 @@
     "cross-env": "^7.0.0",
     "cross-env": "^7.0.0",
     "dotenv-flow": "^3.2.0",
     "dotenv-flow": "^3.2.0",
     "npm-run-all": "^4.1.5",
     "npm-run-all": "^4.1.5",
-    "ts-node": "^9.1.1",
-    "tsconfig-paths": "^3.9.0",
-    "tslib": "^2.3.1",
-    "typescript": "^4.2.3"
+    "tslib": "^2.3.1"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@types/jest": "^26.0.22",
     "@types/jest": "^26.0.22",
@@ -67,7 +67,10 @@
     "lerna": "^4.0.0",
     "lerna": "^4.0.0",
     "rewire": "^5.0.0",
     "rewire": "^5.0.0",
     "shipjs": "^0.23.3",
     "shipjs": "^0.23.3",
-    "ts-jest": "^27.0.4"
+    "ts-jest": "^27.0.4",
+    "ts-node": "^9.1.1",
+    "tsconfig-paths": "^3.9.0",
+    "typescript": "^4.2.3"
   },
   },
   "engines": {
   "engines": {
     "node": "^12 || ^14",
     "node": "^12 || ^14",

+ 2 - 0
packages/app/.env.development

@@ -2,6 +2,8 @@
 ## Handled by Next.js with dotenv or dotenv-flow
 ## Handled by Next.js with dotenv or dotenv-flow
 ## https://nextjs.org/docs/basic-features/environment-variables
 ## https://nextjs.org/docs/basic-features/environment-variables
 ##
 ##
+MIGRATIONS_DIR=src/migrations/
+
 FILE_UPLOAD=mongodb
 FILE_UPLOAD=mongodb
 # MONGO_GRIDFS_TOTAL_LIMIT=10485760
 # MONGO_GRIDFS_TOTAL_LIMIT=10485760
 MATHJAX=1
 MATHJAX=1

+ 1 - 0
packages/app/.env.production

@@ -3,3 +3,4 @@
 ## https://nextjs.org/docs/basic-features/environment-variables
 ## https://nextjs.org/docs/basic-features/environment-variables
 ##
 ##
 FORMAT_NODE_LOG=false
 FORMAT_NODE_LOG=false
+MIGRATIONS_DIR=dist/migrations/

+ 2 - 1
packages/app/.eslintrc.js

@@ -25,11 +25,12 @@ module.exports = {
       name: 'axios',
       name: 'axios',
       message: 'Please use src/utils/axios instead.',
       message: 'Please use src/utils/axios instead.',
     }],
     }],
+    '@typescript-eslint/no-var-requires': 'off',
+
     // set 'warn' temporarily -- 2021.08.02 Yuki Takei
     // set 'warn' temporarily -- 2021.08.02 Yuki Takei
     '@typescript-eslint/explicit-module-boundary-types': ['warn'],
     '@typescript-eslint/explicit-module-boundary-types': ['warn'],
     '@typescript-eslint/no-use-before-define': ['warn'],
     '@typescript-eslint/no-use-before-define': ['warn'],
     '@typescript-eslint/no-this-alias': ['warn'],
     '@typescript-eslint/no-this-alias': ['warn'],
-    '@typescript-eslint/no-var-requires': ['warn'],
     'jest/no-done-callback': ['warn'],
     'jest/no-done-callback': ['warn'],
   },
   },
 };
 };

+ 1 - 1
packages/app/.gitignore

@@ -5,8 +5,8 @@
 # dist
 # dist
 /dist/
 /dist/
 /transpiled/
 /transpiled/
+/report/
 /public/static/js
 /public/static/js
-/public/static/dict
 /public/static/styles
 /public/static/styles
 /public/uploads
 /public/uploads
 /tmp/
 /tmp/

+ 1 - 12
packages/app/bin/cdn/cdn-resources-downloader.ts

@@ -4,9 +4,7 @@ import urljoin from 'url-join';
 import { Transform } from 'stream';
 import { Transform } from 'stream';
 import replaceStream from 'replacestream';
 import replaceStream from 'replacestream';
 
 
-import {
-  cdnLocalScriptRoot, cdnLocalDictRoot, cdnLocalStyleRoot, cdnLocalStyleWebRoot,
-} from '^/config/cdn';
+import { cdnLocalScriptRoot, cdnLocalStyleRoot, cdnLocalStyleWebRoot } from '^/config/cdn';
 import * as cdnManifests from '^/resource/cdn-manifests';
 import * as cdnManifests from '^/resource/cdn-manifests';
 
 
 import { CdnResource, CdnManifest } from '~/interfaces/cdn';
 import { CdnResource, CdnManifest } from '~/interfaces/cdn';
@@ -22,14 +20,6 @@ export default class CdnResourcesDownloader {
       return { manifest, outDir: cdnLocalScriptRoot };
       return { manifest, outDir: cdnLocalScriptRoot };
     });
     });
 
 
-    const cdnDictResources: CdnResource[] = cdnManifests.dict.map((manifest: CdnManifest) => {
-      return { manifest, outDir: cdnLocalDictRoot };
-    });
-
-    const dictExtensionOptions = {
-      ext: 'gz',
-    };
-
     const cdnStyleResources: CdnResource[] = cdnManifests.style.map((manifest) => {
     const cdnStyleResources: CdnResource[] = cdnManifests.style.map((manifest) => {
       return { manifest, outDir: cdnLocalStyleRoot };
       return { manifest, outDir: cdnLocalStyleRoot };
     });
     });
@@ -42,7 +32,6 @@ export default class CdnResourcesDownloader {
 
 
     return Promise.all([
     return Promise.all([
       this.downloadScripts(cdnScriptResources),
       this.downloadScripts(cdnScriptResources),
-      this.downloadScripts(cdnDictResources, dictExtensionOptions),
       this.downloadStyles(cdnStyleResources, dlStylesOptions),
       this.downloadStyles(cdnStyleResources, dlStylesOptions),
     ]);
     ]);
   }
   }

+ 0 - 1
packages/app/config/cdn.js

@@ -4,6 +4,5 @@ import { projectRoot } from '~/utils/project-dir-utils';
 
 
 export const cdnLocalScriptRoot = path.join(projectRoot, 'public/static/js/cdn');
 export const cdnLocalScriptRoot = path.join(projectRoot, 'public/static/js/cdn');
 export const cdnLocalScriptWebRoot = '/static/js/cdn';
 export const cdnLocalScriptWebRoot = '/static/js/cdn';
-export const cdnLocalDictRoot = path.join(projectRoot, 'public/static/dict/cdn');
 export const cdnLocalStyleRoot = path.join(projectRoot, 'public/static/styles/cdn');
 export const cdnLocalStyleRoot = path.join(projectRoot, 'public/static/styles/cdn');
 export const cdnLocalStyleWebRoot = '/static/styles/cdn';
 export const cdnLocalStyleWebRoot = '/static/styles/cdn';

+ 2 - 2
packages/app/config/webpack.prod.js

@@ -15,7 +15,7 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
 /**
 /**
   * Webpack Constants
   * Webpack Constants
   */
   */
-const { ANALYZE } = process.env;
+const { ANALYZE_BUNDLE_SIZE } = process.env;
 
 
 module.exports = require('./webpack.common')({
 module.exports = require('./webpack.common')({
   mode: 'production',
   mode: 'production',
@@ -60,7 +60,7 @@ module.exports = require('./webpack.common')({
     }),
     }),
 
 
     new BundleAnalyzerPlugin({
     new BundleAnalyzerPlugin({
-      analyzerMode: ANALYZE ? 'static' : 'disabled',
+      analyzerMode: ANALYZE_BUNDLE_SIZE ? 'static' : 'disabled',
       reportFilename: path.resolve(__dirname, '../report/bundle-analyzer.html'),
       reportFilename: path.resolve(__dirname, '../report/bundle-analyzer.html'),
       openAnalyzer: false,
       openAnalyzer: false,
     }),
     }),

+ 6 - 11
packages/app/docker/Dockerfile

@@ -72,15 +72,14 @@ RUN rm node_modules.tar
 ##
 ##
 FROM prebuilder-default AS prebuilder-nocdn
 FROM prebuilder-default AS prebuilder-nocdn
 
 
-# replace env.prod.js for NO_CDN
-COPY docker/nocdn/env.prod.js ${appDir}/config/
+# add dotenv file for NO_CDN
+COPY packages/app/docker/nocdn/.env.production.local ${appDir}/packages/app/
 
 
 
 
 
 
 ##
 ##
 ## builder
 ## builder
 ##
 ##
-# FROM prebuilder-${flavor}
 FROM prebuilder-${flavor} AS builder
 FROM prebuilder-${flavor} AS builder
 
 
 ENV appDir /opt/growi
 ENV appDir /opt/growi
@@ -113,7 +112,8 @@ RUN tar cf packages.tar \
   packages/app/public \
   packages/app/public \
   packages/app/resource \
   packages/app/resource \
   packages/app/tmp \
   packages/app/tmp \
-  packages/app/.env.production \
+  packages/app/migrate-mongo-config.js \
+  packages/app/.env.production* \
   packages/app/tsconfig.base.json \
   packages/app/tsconfig.base.json \
   packages/app/tsconfig.json \
   packages/app/tsconfig.json \
   packages/*/package.json \
   packages/*/package.json \
@@ -140,11 +140,6 @@ RUN set -eux; \
 # verify that the binary works
 # verify that the binary works
 	gosu nobody true
 	gosu nobody true
 
 
-# Add Tini
-ENV TINI_VERSION v0.19.0
-ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
-RUN chmod +x /tini
-
 COPY --from=deps-resolver-prod --chown=node:node \
 COPY --from=deps-resolver-prod --chown=node:node \
   ${appDir}/node_modules.tar ${appDir}/
   ${appDir}/node_modules.tar ${appDir}/
 COPY --from=builder --chown=node:node \
 COPY --from=builder --chown=node:node \
@@ -168,5 +163,5 @@ WORKDIR ${appDir}/packages/app
 VOLUME /data
 VOLUME /data
 EXPOSE 3000
 EXPOSE 3000
 
 
-ENTRYPOINT ["/tini", "-e", "143", "--", "/docker-entrypoint.sh"]
-CMD ["node", "-r", "dotenv-flow/config", "--expose_gc", "dist/server/app.js"]
+ENTRYPOINT ["/docker-entrypoint.sh"]
+CMD ["yarn migrate && node -r dotenv-flow/config --expose_gc dist/server/app.js"]

+ 6 - 0
packages/app/docker/Dockerfile.dockerignore

@@ -0,0 +1,6 @@
+node_modules
+*/node_modules
+*/coverage
+*/dist
+*/Dockerfile
+*/*.dockerignore

+ 1 - 1
packages/app/docker/docker-entrypoint.sh

@@ -11,4 +11,4 @@ fi
 chown -R node:node /data/uploads
 chown -R node:node /data/uploads
 chown -h node:node ./public/uploads
 chown -h node:node ./public/uploads
 
 
-gosu node $@
+exec gosu node /bin/bash -c "$@"

+ 6 - 0
packages/app/docker/nocdn/.env.production.local

@@ -0,0 +1,6 @@
+
+##
+## Handled by Next.js with dotenv or dotenv-flow
+## https://nextjs.org/docs/basic-features/environment-variables
+##
+NO_CDN=true

+ 0 - 5
packages/app/docker/nocdn/env.prod.js

@@ -1,5 +0,0 @@
-module.exports = {
-  NODE_ENV: 'production',
-  NO_CDN: true,
-  // FORMAT_NODE_LOG: false,
-};

+ 21 - 5
packages/app/jest.config.js

@@ -14,10 +14,22 @@ module.exports = {
 
 
   preset: 'ts-jest/presets/js-with-ts',
   preset: 'ts-jest/presets/js-with-ts',
 
 
-  globalSetup: '<rootDir>/src/test/global-setup.js',
-  globalTeardown: '<rootDir>/src/test/global-teardown.js',
-
   projects: [
   projects: [
+    {
+      displayName: 'unit',
+
+      preset: 'ts-jest/presets/js-with-ts',
+
+      rootDir: '.',
+      roots: ['<rootDir>/src'],
+      testMatch: ['<rootDir>/src/test/unit/**/*.test.ts', '<rootDir>/src/test/unit/**/*.test.js'],
+
+      testEnvironment: 'node',
+
+      // Automatically clear mock calls and instances between every test
+      clearMocks: true,
+      moduleNameMapper: MODULE_NAME_MAPPING,
+    },
     {
     {
       displayName: 'server',
       displayName: 'server',
 
 
@@ -25,9 +37,13 @@ module.exports = {
 
 
       rootDir: '.',
       rootDir: '.',
       roots: ['<rootDir>/src'],
       roots: ['<rootDir>/src'],
+      testMatch: ['<rootDir>/src/test/integration/**/*.test.ts', '<rootDir>/src/test/integration/**/*.test.js'],
+
       testEnvironment: 'node',
       testEnvironment: 'node',
-      setupFilesAfterEnv: ['<rootDir>/src/test/setup.js'],
-      testMatch: ['<rootDir>/src/test/**/*.test.ts', '<rootDir>/src/test/**/*.test.js'],
+      globalSetup: '<rootDir>/src/test/integration/global-setup.js',
+      globalTeardown: '<rootDir>/src/test/integration/global-teardown.js',
+      setupFilesAfterEnv: ['<rootDir>/src/test/integration/setup.js'],
+
       // Automatically clear mock calls and instances between every test
       // Automatically clear mock calls and instances between every test
       clearMocks: true,
       clearMocks: true,
       moduleNameMapper: MODULE_NAME_MAPPING,
       moduleNameMapper: MODULE_NAME_MAPPING,

+ 8 - 5
packages/app/config/migrate.js → packages/app/migrate-mongo-config.js

@@ -5,11 +5,15 @@
  * @author Yuki Takei <yuki@weseek.co.jp>
  * @author Yuki Takei <yuki@weseek.co.jp>
  */
  */
 
 
-import mongoose from 'mongoose';
+const { URL } = require('url');
 
 
-import { initMongooseGlobalSettings, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+// get migrationsDir from env var
+const migrationsDir = process.env.MIGRATIONS_DIR;
+if (migrationsDir == null) {
+  throw new Error('An env var MIGRATIONS_DIR must be set.');
+}
 
 
-const { URL } = require('url');
+const { initMongooseGlobalSettings, getMongoUri, mongoOptions } = require('@growi/core');
 
 
 initMongooseGlobalSettings();
 initMongooseGlobalSettings();
 
 
@@ -25,8 +29,7 @@ const mongodb = {
 };
 };
 
 
 module.exports = {
 module.exports = {
-  mongoUri,
   mongodb,
   mongodb,
-  migrationsDir: 'src/migrations/',
+  migrationsDir,
   changelogCollectionName: 'migrations',
   changelogCollectionName: 'migrations',
 };
 };

+ 16 - 13
packages/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "4.4.6",
+  "version": "4.4.7-RC.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -14,13 +14,20 @@
     "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config --expose_gc dist/server/app.js",
     "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config --expose_gc dist/server/app.js",
     "server:ci": "yarn server --ci",
     "server:ci": "yarn server --ci",
     "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
     "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
+    "migrate": "node -r dotenv-flow/config node_modules/.bin/migrate-mongo up",
     "//// for development": "",
     "//// for development": "",
     "dev": "run-p dev:client dev:server",
     "dev": "run-p dev:client dev:server",
     "dev:client": "yarn cross-env NODE_ENV=development webpack --config config/webpack.dev.js --progress --watch",
     "dev:client": "yarn cross-env NODE_ENV=development webpack --config config/webpack.dev.js --progress --watch",
     "dev:client:nowatch": "yarn cross-env NODE_ENV=development webpack --config config/webpack.dev.js",
     "dev:client:nowatch": "yarn cross-env NODE_ENV=development webpack --config config/webpack.dev.js",
     "dev:server": "yarn cross-env NODE_ENV=development ts-node-dev --inspect --expose-gc -r tsconfig-paths/register -r dotenv-flow/config --transpile-only src/server/app.ts",
     "dev:server": "yarn cross-env NODE_ENV=development ts-node-dev --inspect --expose-gc -r tsconfig-paths/register -r dotenv-flow/config --transpile-only src/server/app.ts",
     "predev:client": "yarn cross-env NODE_ENV=development run-p resources:*",
     "predev:client": "yarn cross-env NODE_ENV=development run-p resources:*",
-    "predev:server": "yarn cross-env NODE_ENV=development yarn migrate",
+    "predev:server": "yarn cross-env NODE_ENV=development yarn dev:migrate:up",
+    "dev:migrate-mongo": "yarn cross-env NODE_ENV=development yarn ts-node node_modules/.bin/migrate-mongo",
+    "dev:migrate": "yarn dev:migrate:up",
+    "dev:migrate:create": "yarn dev:migrate-mongo create",
+    "dev:migrate:status": "yarn dev:migrate-mongo status",
+    "dev:migrate:up": "yarn dev:migrate-mongo up",
+    "dev:migrate:down": "yarn dev:migrate-mongo down",
     "//// for CI": "",
     "//// for CI": "",
     "dev:ci": "yarn dev:client:nowatch && yarn dev:server --ci",
     "dev:ci": "yarn dev:client:nowatch && yarn dev:server --ci",
     "predev:ci": "run-p resources:*",
     "predev:ci": "run-p resources:*",
@@ -39,11 +46,6 @@
     "openapi:v1": "yarn cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
     "openapi:v1": "yarn cross-env API_VERSION=1 yarn swagger-jsdoc -- \"src/server/*/*.js\" \"src/server/models/**/*.js\"",
     "resources:plugin": "yarn ts-node bin/generate-plugin-definitions-source.ts",
     "resources:plugin": "yarn ts-node bin/generate-plugin-definitions-source.ts",
     "resources:dl-resources": "yarn ts-node bin/download-cdn-resources.ts",
     "resources:dl-resources": "yarn ts-node bin/download-cdn-resources.ts",
-    "migrate": "yarn migrate:up",
-    "migrate:create": "yarn ts-node node_modules/.bin/migrate-mongo create -f config/migrate.js",
-    "migrate:status": "yarn ts-node node_modules/.bin/migrate-mongo status -f config/migrate.js",
-    "migrate:up": "yarn ts-node node_modules/.bin/migrate-mongo up -f config/migrate.js",
-    "migrate:down": "yarn ts-node node_modules/.bin/migrate-mongo down -f config/migrate.js",
     "ts-node": "ts-node -r tsconfig-paths/register -r dotenv-flow/config --transpile-only"
     "ts-node": "ts-node -r tsconfig-paths/register -r dotenv-flow/config --transpile-only"
   },
   },
   "// comments for dependencies": {
   "// comments for dependencies": {
@@ -53,12 +55,13 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
+    "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^4.4.6",
-    "@growi/plugin-attachment-refs": "^4.4.6",
-    "@growi/plugin-pukiwiki-like-linker": "^4.4.6",
-    "@growi/plugin-lsx": "^4.4.6",
-    "@growi/slack": "^4.4.6",
+    "@growi/codemirror-textlint": "^4.4.7-RC.0",
+    "@growi/plugin-attachment-refs": "^4.4.7-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^4.4.7-RC.0",
+    "@growi/plugin-lsx": "^4.4.7-RC.0",
+    "@growi/slack": "^4.4.7-RC.0",
     "@promster/express": "^5.1.0",
     "@promster/express": "^5.1.0",
     "@promster/server": "^6.0.3",
     "@promster/server": "^6.0.3",
     "@slack/events-api": "^3.0.0",
     "@slack/events-api": "^3.0.0",
@@ -154,7 +157,7 @@
     "@alienfast/i18next-loader": "^1.0.16",
     "@alienfast/i18next-loader": "^1.0.16",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/navigation-next": "^8.0.5",
     "@atlaskit/navigation-next": "^8.0.5",
-    "@growi/ui": "^4.4.6",
+    "@growi/ui": "^4.4.7-RC.0",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
     "@types/express": "^4.17.11",

+ 3 - 0
packages/app/public/static/dict/base.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0803327762e1c93ca731e4319ab8343340f2806bb84941207782cde9d2d5a8eb
+size 3956825

+ 3 - 0
packages/app/public/static/dict/cc.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:02b7631be0d4de3a1a75cd9f9cc51536e4f94c9e6b389b813e06ba0f6e7de765
+size 1692067

+ 3 - 0
packages/app/public/static/dict/check.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:193ae0035fff6fe812b58d9ee730e7a7d7ee601d918481ce51075c58114f6cc9
+size 3111633

+ 3 - 0
packages/app/public/static/dict/tid.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d43d831cb6fb0f0a411739cd287a6d5e998e121a8daca614df14a81a0dcac586
+size 1605820

+ 3 - 0
packages/app/public/static/dict/tid_map.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:33efd5ffd87a70f669add093fa39dee44341d58f940844ef107c8fd98bb795b2
+size 1485576

+ 3 - 0
packages/app/public/static/dict/tid_pos.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:60dbfc99a6ab993f30c5dab648bec6ad7f9aaefa5c14e1843837d95e509f8895
+size 5916009

+ 3 - 0
packages/app/public/static/dict/unk.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f7f991cdeb9bfd3e9c0e4577cc50ee0815a11c508cccd444a9d3ab3c81521100
+size 10512

+ 3 - 0
packages/app/public/static/dict/unk_char.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9a8e86fd9aff32d323fbb59f5a7006f05927a11f8173c90712cc56293aeb3225
+size 306

+ 3 - 0
packages/app/public/static/dict/unk_compat.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:50f60aa29bc2e86c2903ab8c825bb6fa604d2b294d96941c1d3924259791899d
+size 338

+ 3 - 0
packages/app/public/static/dict/unk_invoke.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6b210889548457c3006913afd12c8b525562255f2709e404604be9614a25e94c
+size 1140

+ 3 - 0
packages/app/public/static/dict/unk_map.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6df12460e5477230bb6fd9641def918b699fc0a8868016b6c9f794488630509b
+size 1190

+ 3 - 0
packages/app/public/static/dict/unk_pos.dat.gz

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5b183a29f281acc7e0542beca47b83f7985047c0a2d27e78a66f32276be5ad11
+size 10540

+ 0 - 86
packages/app/resource/cdn-manifests.js

@@ -89,92 +89,6 @@ module.exports = {
       },
       },
     },
     },
   ],
   ],
-  dict: [
-    {
-      name: 'base.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/base.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'cc.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/cc.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'check.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/check.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'tid_map.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/tid_map.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'tid_pos.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/tid_pos.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'tid.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/tid.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'unk_char.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_char.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'unk_compat.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_compat.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'unk_invoke.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_invoke.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'unk_map.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_map.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'unk_pos.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk_pos.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-    {
-      name: 'unk.dat',
-      url: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/unk.dat.gz',
-      args: {
-        integrity: '',
-      },
-    },
-  ],
   style: [
   style: [
     {
     {
       name: 'lato',
       name: 'lato',

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

@@ -65,7 +65,7 @@ class SamlSecurityManagementContents extends React.Component {
                 {t('security_setting.SAML.enable_saml')}
                 {t('security_setting.SAML.enable_saml')}
               </label>
               </label>
             </div>
             </div>
-            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('ldap') && isSamlEnabled)
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('saml') && isSamlEnabled)
               && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
               && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
           </div>
         </div>
         </div>

+ 4 - 4
packages/app/src/components/Me/EditorSettings.tsx

@@ -46,10 +46,10 @@ const commonRulesMenuItems = [
     name: 'sentence-length',
     name: 'sentence-length',
     description: 'editor_settings.common_settings.sentence_length',
     description: 'editor_settings.common_settings.sentence_length',
   },
   },
-  {
-    name: 'en-capitalization',
-    description: 'editor_settings.common_settings.en_capitalization',
-  },
+  // {  // omit because en-pos package is too big
+  //   name: 'en-capitalization',
+  //   description: 'editor_settings.common_settings.en_capitalization',
+  // },
   {
   {
     name: 'no-unmatched-pair',
     name: 'no-unmatched-pair',
     description: 'editor_settings.common_settings.no_unmatched_pair',
     description: 'editor_settings.common_settings.no_unmatched_pair',

+ 2 - 5
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -33,8 +33,9 @@ import HandsontableModal from './HandsontableModal';
 import EditorIcon from './EditorIcon';
 import EditorIcon from './EditorIcon';
 import DrawioModal from './DrawioModal';
 import DrawioModal from './DrawioModal';
 
 
-
+// Textlint
 window.JSHINT = JSHINT;
 window.JSHINT = JSHINT;
+window.kuromojin = { dicPath: '/static/dict' };
 
 
 // set save handler
 // set save handler
 codemirror.commands.save = (instance) => {
 codemirror.commands.save = (instance) => {
@@ -154,10 +155,6 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0';
     this.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.42.0';
     this.cmNoCdnScriptRoot = '/static/js/cdn';
     this.cmNoCdnScriptRoot = '/static/js/cdn';
     this.cmNoCdnStyleRoot = '/static/styles/cdn';
     this.cmNoCdnStyleRoot = '/static/styles/cdn';
-    window.kuromojin = this.props.noCdn
-      ? { dicPath: '/static/dict/cdn' }
-      : { dicPath: 'https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict' };
-
     this.interceptorManager = new InterceptorManager();
     this.interceptorManager = new InterceptorManager();
     this.interceptorManager.addInterceptors([
     this.interceptorManager.addInterceptors([
       new PreventMarkdownListInterceptor(),
       new PreventMarkdownListInterceptor(),

+ 1 - 1
packages/app/src/components/Sidebar/RecentChanges.jsx

@@ -187,7 +187,7 @@ class RecentChanges extends React.Component {
               className="custom-control-input"
               className="custom-control-input"
               type="checkbox"
               type="checkbox"
               checked={this.state.isRecentChangesSidebarSmall}
               checked={this.state.isRecentChangesSidebarSmall}
-              onChange={e => this.setState({ isRecentChangesSidebarSmall: e.target.checked })}
+              onChange={this.changeSizeHandler}
             />
             />
             <label className="custom-control-label" htmlFor="recentChangesResize">
             <label className="custom-control-label" htmlFor="recentChangesResize">
             </label>
             </label>

+ 2 - 3
packages/app/src/migrations/20180926134048-make-email-unique.js

@@ -1,8 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { getModelSafely } from '~/server/util/mongoose-utils';
 
 
 const logger = loggerFactory('growi:migrate:make-email-unique');
 const logger = loggerFactory('growi:migrate:make-email-unique');
 
 
@@ -10,7 +9,7 @@ module.exports = {
 
 
   async up(db, next) {
   async up(db, next) {
     logger.info('Start migration');
     logger.info('Start migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const User = getModelSafely('User') || require('~/server/models/user')();
     const User = getModelSafely('User') || require('~/server/models/user')();
 
 

+ 3 - 3
packages/app/src/migrations/20180927102719-init-serverurl.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:init-serverurl');
 const logger = loggerFactory('growi:migrate:init-serverurl');
@@ -20,7 +20,7 @@ module.exports = {
 
 
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     // find 'app:siteUrl'
     // find 'app:siteUrl'
     const siteUrlConfig = await Config.findOne({
     const siteUrlConfig = await Config.findOne({
@@ -77,7 +77,7 @@ module.exports = {
 
 
   async down(db) {
   async down(db) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     // remote 'app:siteUrl'
     // remote 'app:siteUrl'
     await Config.findOneAndDelete({
     await Config.findOneAndDelete({

+ 3 - 4
packages/app/src/migrations/20181019114028-abolish-page-group-relation.js

@@ -1,8 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { getModelSafely } from '~/server/util/mongoose-utils';
 
 
 const logger = loggerFactory('growi:migrate:abolish-page-group-relation');
 const logger = loggerFactory('growi:migrate:abolish-page-group-relation');
 
 
@@ -29,7 +28,7 @@ module.exports = {
 
 
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const isPagegrouprelationsExists = await isCollectionExists(db, 'pagegrouprelations');
     const isPagegrouprelationsExists = await isCollectionExists(db, 'pagegrouprelations');
     if (!isPagegrouprelationsExists) {
     if (!isPagegrouprelationsExists) {
@@ -73,7 +72,7 @@ module.exports = {
 
 
   async down(db) {
   async down(db) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const Page = getModelSafely('Page') || require('~/server/models/page')();
     const Page = getModelSafely('Page') || require('~/server/models/page')();
     const UserGroup = getModelSafely('UserGroup') || require('~/server/models/user-group')();
     const UserGroup = getModelSafely('UserGroup') || require('~/server/models/user-group')();

+ 2 - 2
packages/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:abolish-crowi-classic-auth');
 const logger = loggerFactory('growi:migrate:abolish-crowi-classic-auth');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:abolish-crowi-classic-auth');
 module.exports = {
 module.exports = {
   async up(db, next) {
   async up(db, next) {
     logger.info('Start migration');
     logger.info('Start migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     // enable passport and delete configs for crowi classic auth
     // enable passport and delete configs for crowi classic auth
     await Promise.all([
     await Promise.all([

+ 3 - 4
packages/app/src/migrations/20190618104011-add-config-app-installed.js

@@ -1,9 +1,8 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { getModelSafely } from '~/server/util/mongoose-utils';
 
 
 const logger = loggerFactory('growi:migrate:add-config-app-installed');
 const logger = loggerFactory('growi:migrate:add-config-app-installed');
 
 
@@ -19,7 +18,7 @@ module.exports = {
 
 
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const User = getModelSafely('User') || require('~/server/models/user')();
     const User = getModelSafely('User') || require('~/server/models/user')();
 
 
@@ -49,7 +48,7 @@ module.exports = {
 
 
   async down(db) {
   async down(db) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     // remote 'app:siteUrl'
     // remote 'app:siteUrl'
     await Config.findOneAndDelete({
     await Config.findOneAndDelete({

+ 2 - 2
packages/app/src/migrations/20190619055421-adjust-page-grant.js

@@ -1,6 +1,6 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:adjust-page-grant');
 const logger = loggerFactory('growi:migrate:adjust-page-grant');
@@ -9,7 +9,7 @@ module.exports = {
 
 
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const Page = require('~/server/models/page')();
     const Page = require('~/server/models/page')();
 
 

+ 2 - 2
packages/app/src/migrations/20190624110950-fill-last-update-user.js

@@ -1,6 +1,6 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:abolish-page-group-relation');
 const logger = loggerFactory('growi:migrate:abolish-page-group-relation');
@@ -12,7 +12,7 @@ module.exports = {
 
 
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const Page = require('~/server/models/page')();
     const Page = require('~/server/models/page')();
 
 

+ 2 - 2
packages/app/src/migrations/20190629193445-make-root-page-public.js

@@ -1,6 +1,6 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:make-root-page-public');
 const logger = loggerFactory('growi:migrate:make-root-page-public');
@@ -8,7 +8,7 @@ const logger = loggerFactory('growi:migrate:make-root-page-public');
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const Page = require('~/server/models/page')();
     const Page = require('~/server/models/page')();
 
 

+ 2 - 2
packages/app/src/migrations/20191102223900-drop-configs-indices.js

@@ -1,6 +1,6 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:drop-configs-indices');
 const logger = loggerFactory('growi:migrate:drop-configs-indices');
@@ -14,7 +14,7 @@ async function dropIndexIfExists(collection, indexName) {
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const collection = db.collection('configs');
     const collection = db.collection('configs');
     await dropIndexIfExists(collection, 'ns_1');
     await dropIndexIfExists(collection, 'ns_1');

+ 2 - 2
packages/app/src/migrations/20191102223901-drop-pages-indices.js

@@ -1,6 +1,6 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:drop-pages-indices');
 const logger = loggerFactory('growi:migrate:drop-pages-indices');
@@ -21,7 +21,7 @@ async function dropIndexIfExists(db, collectionName, indexName) {
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     await dropIndexIfExists(db, 'pages', 'lastUpdateUser_1');
     await dropIndexIfExists(db, 'pages', 'lastUpdateUser_1');
     await dropIndexIfExists(db, 'pages', 'liker_1');
     await dropIndexIfExists(db, 'pages', 'liker_1');

+ 3 - 3
packages/app/src/migrations/20191126173016-adjust-pages-path.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
-import { pathUtils } from '@growi/core';
+import { pathUtils, getMongoUri, mongoOptions } from '@growi/core';
+
 
 
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:adjust-pages-path');
 const logger = loggerFactory('growi:migrate:adjust-pages-path');
@@ -10,7 +10,7 @@ const logger = loggerFactory('growi:migrate:adjust-pages-path');
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const Page = require('~/server/models/page')();
     const Page = require('~/server/models/page')();
 
 

+ 2 - 2
packages/app/src/migrations/20191127023815-drop-wrong-index-of-page-tag-relation.js

@@ -1,6 +1,6 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:drop-wrong-index-of-page-tag-relation');
 const logger = loggerFactory('growi:migrate:drop-wrong-index-of-page-tag-relation');
@@ -21,7 +21,7 @@ async function dropIndexIfExists(db, collectionName, indexName) {
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     await dropIndexIfExists(db, 'pagetagrelations', 'page_1_user_1');
     await dropIndexIfExists(db, 'pagetagrelations', 'page_1_user_1');
 
 

+ 2 - 3
packages/app/src/migrations/20200402160380-remove-deleteduser-from-relationgroup.js

@@ -1,15 +1,14 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { getModelSafely } from '~/server/util/mongoose-utils';
 
 
 const logger = loggerFactory('growi:migrate:remove-deleteduser-from-relationgroup');
 const logger = loggerFactory('growi:migrate:remove-deleteduser-from-relationgroup');
 
 
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const User = getModelSafely('User') || require('~/server/models/user')();
     const User = getModelSafely('User') || require('~/server/models/user')();
     const UserGroupRelation = getModelSafely('UserGroupRelation') || require('~/server/models/user-group-relation')();
     const UserGroupRelation = getModelSafely('UserGroupRelation') || require('~/server/models/user-group-relation')();

+ 2 - 2
packages/app/src/migrations/20200420160390-remove-crowi-layout.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:remove-crowi-lauout');
 const logger = loggerFactory('growi:migrate:remove-crowi-lauout');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:remove-crowi-lauout');
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const query = { key: 'customize:layout', value: JSON.stringify('crowi') };
     const query = { key: 'customize:layout', value: JSON.stringify('crowi') };
 
 

+ 3 - 3
packages/app/src/migrations/20200512005851-remove-behavior-type.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:remove-behavior-type');
 const logger = loggerFactory('growi:migrate:remove-behavior-type');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:remove-behavior-type');
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     await Config.findOneAndDelete({ key: 'customize:behavior' }); // remove behavior
     await Config.findOneAndDelete({ key: 'customize:behavior' }); // remove behavior
 
 
@@ -19,7 +19,7 @@ module.exports = {
   async down(db, client) {
   async down(db, client) {
     // do not rollback
     // do not rollback
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const insertConfig = new Config({
     const insertConfig = new Config({
       ns: 'crowi',
       ns: 'crowi',

+ 2 - 2
packages/app/src/migrations/20200514001356-update-theme-color-for-dark.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:update-theme-color-for-dark');
 const logger = loggerFactory('growi:migrate:update-theme-color-for-dark');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:update-theme-color-for-dark');
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     await Promise.all([
     await Promise.all([
       await Config.findOneAndUpdate({ key: 'customize:theme', value: JSON.stringify('default-dark') }, { value: JSON.stringify('default') }), // update default-dark
       await Config.findOneAndUpdate({ key: 'customize:theme', value: JSON.stringify('default-dark') }, { value: JSON.stringify('default') }), // update default-dark

+ 2 - 3
packages/app/src/migrations/20200620203632-normalize-locale-id.js

@@ -1,16 +1,15 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { getModelSafely } from '~/server/util/mongoose-utils';
 
 
 const logger = loggerFactory('growi:migrate:normalize-locale-id');
 const logger = loggerFactory('growi:migrate:normalize-locale-id');
 
 
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const User = getModelSafely('User') || require('~/server/models/user')();
     const User = getModelSafely('User') || require('~/server/models/user')();
 
 

+ 3 - 3
packages/app/src/migrations/20200827045151-remove-layout-setting.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:remove-layout-setting');
 const logger = loggerFactory('growi:migrate:remove-layout-setting');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:remove-layout-setting');
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const layoutType = await Config.findOne({ key: 'customize:layout' });
     const layoutType = await Config.findOne({ key: 'customize:layout' });
 
 
@@ -38,7 +38,7 @@ module.exports = {
 
 
   async down(db, client) {
   async down(db, client) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const theme = await Config.findOne({ key: 'customize:theme' });
     const theme = await Config.findOne({ key: 'customize:theme' });
     const insertLayoutType = (theme.value === '"kibela"') ? 'kibela' : 'growi';
     const insertLayoutType = (theme.value === '"kibela"') ? 'kibela' : 'growi';

+ 3 - 3
packages/app/src/migrations/20200828024025-copy-aws-setting.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:remove-layout-setting');
 const logger = loggerFactory('growi:migrate:remove-layout-setting');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:remove-layout-setting');
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const [accessKeyId, secretAccessKey] = await Promise.all([
     const [accessKeyId, secretAccessKey] = await Promise.all([
       Config.findOne({ key: 'aws:accessKeyId' }),
       Config.findOne({ key: 'aws:accessKeyId' }),
@@ -55,7 +55,7 @@ module.exports = {
 
 
   async down(db, client) {
   async down(db, client) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     await Config.deleteMany({ key: { $in: ['mail:sesAccessKeyId', 'mail:sesSecretAccessKey'] } });
     await Config.deleteMany({ key: { $in: ['mail:sesAccessKeyId', 'mail:sesSecretAccessKey'] } });
 
 

+ 3 - 3
packages/app/src/migrations/20200901034313-update-mail-transmission.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:update-mail-transmission');
 const logger = loggerFactory('growi:migrate:update-mail-transmission');
@@ -9,7 +9,7 @@ const logger = loggerFactory('growi:migrate:update-mail-transmission');
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const sesExist = await Config.findOne({
     const sesExist = await Config.findOne({
       ns: 'crowi',
       ns: 'crowi',
@@ -33,7 +33,7 @@ module.exports = {
 
 
   async down(db, client) {
   async down(db, client) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     // remote 'mail:transmissionMethod'
     // remote 'mail:transmissionMethod'
     await Config.findOneAndDelete({
     await Config.findOneAndDelete({

+ 2 - 2
packages/app/src/migrations/20200901034314-update-mail-transmission-fix.js

@@ -1,3 +1,4 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
@@ -5,12 +6,11 @@ import Config from '~/server/models/config';
 const logger = loggerFactory('growi:migrate:update-mail-transmission-fix');
 const logger = loggerFactory('growi:migrate:update-mail-transmission-fix');
 
 
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
-const config = require('^/config/migrate');
 
 
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const transmissionMethod = await Config.findOne({
     const transmissionMethod = await Config.findOne({
       ns: 'crowi',
       ns: 'crowi',

+ 3 - 3
packages/app/src/migrations/20200903080025-remove-timeline-type.js.js

@@ -1,3 +1,4 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
@@ -5,12 +6,11 @@ import Config from '~/server/models/config';
 const logger = loggerFactory('growi:migrate:remove-timeline-type');
 const logger = loggerFactory('growi:migrate:remove-timeline-type');
 
 
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
-const config = require('^/config/migrate');
 
 
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     await Config.findOneAndDelete({ key: 'customize:isEnabledTimeline' }); // remove timeline
     await Config.findOneAndDelete({ key: 'customize:isEnabledTimeline' }); // remove timeline
 
 
@@ -20,7 +20,7 @@ module.exports = {
   async down(db, client) {
   async down(db, client) {
     // do not rollback
     // do not rollback
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const insertConfig = new Config({
     const insertConfig = new Config({
       ns: 'crowi',
       ns: 'crowi',

+ 3 - 3
packages/app/src/migrations/20200915035234-rename-s3-config.js

@@ -1,3 +1,4 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
@@ -5,7 +6,6 @@ import Config from '~/server/models/config';
 const logger = loggerFactory('growi:migrate:remove-timeline-type');
 const logger = loggerFactory('growi:migrate:remove-timeline-type');
 
 
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
-const config = require('^/config/migrate');
 
 
 const awsConfigs = [
 const awsConfigs = [
   {
   {
@@ -33,7 +33,7 @@ const awsConfigs = [
 module.exports = {
 module.exports = {
   async up(db, client) {
   async up(db, client) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const request = awsConfigs.map((awsConfig) => {
     const request = awsConfigs.map((awsConfig) => {
       return {
       return {
@@ -52,7 +52,7 @@ module.exports = {
   async down(db, client) {
   async down(db, client) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
 
 
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const request = awsConfigs.map((awsConfig) => {
     const request = awsConfigs.map((awsConfig) => {
       return {
       return {

+ 2 - 3
packages/app/src/migrations/20210420160380-convert-double-to-date.js

@@ -1,7 +1,6 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import config from '^/config/migrate';
-import { getModelSafely } from '~/server/util/mongoose-utils';
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:remove-crowi-lauout');
 const logger = loggerFactory('growi:migrate:remove-crowi-lauout');
@@ -9,7 +8,7 @@ const logger = loggerFactory('growi:migrate:remove-crowi-lauout');
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const Page = getModelSafely('Page') || require('~/server/models/page')();
     const Page = getModelSafely('Page') || require('~/server/models/page')();
 
 

+ 35 - 44
packages/app/src/migrations/20210830074539-update-configs-for-slackbot.js

@@ -1,64 +1,55 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { getMongoUri, mongoOptions } from '@growi/core';
 import Config from '~/server/models/config';
 import Config from '~/server/models/config';
-import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:update-configs-for-slackbot');
 const logger = loggerFactory('growi:migrate:update-configs-for-slackbot');
 
 
+// key: oldKey, value: newKey
+const keyMap = {
+  'slackbot:proxyServerUri': 'slackbot:proxyUri',
+  'slackbot:token': 'slackbot:withoutProxy:botToken',
+  'slackbot:signingSecret': 'slackbot:withoutProxy:signingSecret',
+};
+
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
-    await Config.bulkWrite([
-      {
-        updateOne: {
-          filter: { key: 'slackbot:proxyServerUri' },
-          update: { key: 'slackbot:proxyUri' },
-        },
-      },
-      {
-        updateOne: {
-          filter: { key: 'slackbot:token' },
-          update: { key: 'slackbot:withoutProxy:botToken' },
-        },
-      },
-      {
-        updateOne: {
-          filter: { key: 'slackbot:signingSecret' },
-          update: { key: 'slackbot:withoutProxy:signingSecret' },
-        },
-      },
-    ]);
+    for await (const [oldKey, newKey] of Object.entries(keyMap)) {
+      const isExist = (await Config.count({ key: newKey })) > 0;
+
+      // remove old key
+      if (isExist) {
+        await Config.findOneAndRemove({ key: oldKey });
+      }
+      // update with new key
+      else {
+        await Config.findOneAndUpdate({ key: oldKey }, { key: newKey });
+      }
+    }
 
 
     logger.info('Migration has successfully applied');
     logger.info('Migration has successfully applied');
   },
   },
 
 
   async down(db) {
   async down(db) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
-
-    await Config.bulkWrite([
-      {
-        updateOne: {
-          filter: { key: 'slackbot:proxyUri' },
-          update: { key: 'slackbot:proxyServerUri' },
-        },
-      },
-      {
-        updateOne: {
-          filter: { key: 'slackbot:withoutProxy:botToken' },
-          update: { key: 'slackbot:token' },
-        },
-      },
-      {
-        updateOne: {
-          filter: { key: 'slackbot:withoutProxy:signingSecret' },
-          update: { key: 'slackbot:signingSecret' },
-        },
-      },
-    ]);
+    mongoose.connect(getMongoUri(), mongoOptions);
+
+    for await (const [oldKey, newKey] of Object.entries(keyMap)) {
+      const isExist = (await Config.count({ key: oldKey })) > 0;
+
+      // remove new key
+      if (isExist) {
+        await Config.findOneAndRemove({ key: newKey });
+      }
+      // update with old key
+      else {
+        await Config.findOneAndUpdate({ key: newKey }, { key: oldKey });
+      }
+    }
 
 
     logger.info('Migration has successfully applied');
     logger.info('Migration has successfully applied');
   },
   },

+ 3 - 4
packages/app/src/migrations/20210906194521-slack-app-integration-set-default-value.js

@@ -1,8 +1,7 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
 import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse } from '@growi/slack';
 import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse } from '@growi/slack';
-import { getModelSafely } from '~/server/util/mongoose-utils';
-import config from '^/config/migrate';
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:migrate:slack-app-integration-set-default-value');
 const logger = loggerFactory('growi:migrate:slack-app-integration-set-default-value');
@@ -10,7 +9,7 @@ const logger = loggerFactory('growi:migrate:slack-app-integration-set-default-va
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    mongoose.connect(getMongoUri(), mongoOptions);
 
 
     // Add columns + set all default commands if supportedCommandsForBroadcastUse column does not exist
     // Add columns + set all default commands if supportedCommandsForBroadcastUse column does not exist
     const SlackAppIntegration = getModelSafely('SlackAppIntegration') || require('~/server/models/slack-app-integration')();
     const SlackAppIntegration = getModelSafely('SlackAppIntegration') || require('~/server/models/slack-app-integration')();
@@ -18,7 +17,7 @@ module.exports = {
     // Add togetter command if supportedCommandsForBroadcastUse already exists
     // Add togetter command if supportedCommandsForBroadcastUse already exists
     const slackAppIntegrations = await SlackAppIntegration.find();
     const slackAppIntegrations = await SlackAppIntegration.find();
     slackAppIntegrations.forEach(async(doc) => {
     slackAppIntegrations.forEach(async(doc) => {
-      if (!doc.supportedCommandsForSingleUse.includes('togetter')) {
+      if (doc.supportedCommandsForSingleUse != null && !doc.supportedCommandsForSingleUse.includes('togetter')) {
         doc.supportedCommandsForSingleUse.push('togetter');
         doc.supportedCommandsForSingleUse.push('togetter');
       }
       }
       await doc.save();
       await doc.save();

+ 57 - 43
packages/app/src/migrations/20210913153942-migrate-slack-app-integration-schema.js

@@ -1,51 +1,66 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse } from '@growi/slack';
 import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse } from '@growi/slack';
 
 
-import config from '^/config/migrate';
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { getModelSafely } from '~/server/util/mongoose-utils';
 
 
 
 
 const logger = loggerFactory('growi:migrate:update-configs-for-slackbot');
 const logger = loggerFactory('growi:migrate:update-configs-for-slackbot');
 
 
+// create default data
+const defaultDataForBroadcastUse = {};
+defaultSupportedCommandsNameForBroadcastUse.forEach((commandName) => {
+  defaultDataForBroadcastUse[commandName] = false;
+});
+const defaultDataForSingleUse = {};
+defaultSupportedCommandsNameForSingleUse.forEach((commandName) => {
+  defaultDataForSingleUse[commandName] = false;
+});
+
 module.exports = {
 module.exports = {
   async up(db) {
   async up(db) {
     logger.info('Apply migration');
     logger.info('Apply migration');
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const SlackAppIntegration = getModelSafely('SlackAppIntegration') || require('~/server/models/slack-app-integration')();
     const SlackAppIntegration = getModelSafely('SlackAppIntegration') || require('~/server/models/slack-app-integration')();
 
 
     const slackAppIntegrations = await SlackAppIntegration.find();
     const slackAppIntegrations = await SlackAppIntegration.find();
 
 
-    // create default data
-    const defaultDataForBroadcastUse = {};
-    defaultSupportedCommandsNameForBroadcastUse.forEach((commandName) => {
-      defaultDataForBroadcastUse[commandName] = false;
-    });
-    const defaultDataForSingleUse = {};
-    defaultSupportedCommandsNameForSingleUse.forEach((commandName) => {
-      defaultDataForSingleUse[commandName] = false;
-    });
+    if (slackAppIntegrations.length === 0) return;
 
 
     // create operations
     // create operations
     const operations = slackAppIntegrations.map((doc) => {
     const operations = slackAppIntegrations.map((doc) => {
-      const copyForBroadcastUse = { ...defaultDataForBroadcastUse };
-      const copyForSingleUse = { ...defaultDataForSingleUse };
-      // when the document does NOT have supportedCommandsFor... columns
-      if (doc._doc.supportedCommandsForBroadcastUse == null) {
-        defaultSupportedCommandsNameForBroadcastUse.forEach((commandName) => {
+      let copyForBroadcastUse = { ...defaultDataForBroadcastUse };
+      let copyForSingleUse = { ...defaultDataForSingleUse };
+      // when the document already has permissionsFor... colums
+      if (doc._doc.permissionsForBroadcastUseCommands != null) {
+        // merge
+        copyForBroadcastUse = {
+          ...defaultDataForBroadcastUse,
+          ...Object.fromEntries(doc._doc.permissionsForBroadcastUseCommands),
+        };
+        copyForSingleUse = {
+          ...defaultDataForSingleUse,
+          ...Object.fromEntries(doc._doc.permissionsForSingleUseCommands),
+        };
+      }
+      // when the document has supportedCommandsFor... columns
+      else if (doc._doc.supportedCommandsForBroadcastUse != null) {
+        // merge
+        doc._doc.supportedCommandsForBroadcastUse.forEach((commandName) => {
           copyForBroadcastUse[commandName] = true;
           copyForBroadcastUse[commandName] = true;
         });
         });
-        defaultSupportedCommandsNameForSingleUse.forEach((commandName) => {
+        doc._doc.supportedCommandsForSingleUse.forEach((commandName) => {
           copyForSingleUse[commandName] = true;
           copyForSingleUse[commandName] = true;
         });
         });
       }
       }
-      // // when the document has supportedCommandsFor... columns
+      // when the document does NOT have supportedCommandsFor... columns
       else {
       else {
-        doc._doc.supportedCommandsForBroadcastUse.forEach((commandName) => {
+        // turn on all
+        defaultSupportedCommandsNameForBroadcastUse.forEach((commandName) => {
           copyForBroadcastUse[commandName] = true;
           copyForBroadcastUse[commandName] = true;
         });
         });
-        doc._doc.supportedCommandsForSingleUse.forEach((commandName) => {
+        defaultSupportedCommandsNameForSingleUse.forEach((commandName) => {
           copyForSingleUse[commandName] = true;
           copyForSingleUse[commandName] = true;
         });
         });
       }
       }
@@ -53,35 +68,35 @@ module.exports = {
       return {
       return {
         updateOne: {
         updateOne: {
           filter: { _id: doc._id },
           filter: { _id: doc._id },
-          update: [
-            {
-              $set: {
-                permissionsForBroadcastUseCommands: copyForBroadcastUse,
-                permissionsForSingleUseCommands: copyForSingleUse,
-              },
+          update: {
+            $set: {
+              permissionsForBroadcastUseCommands: copyForBroadcastUse,
+              permissionsForSingleUseCommands: copyForSingleUse,
             },
             },
-            {
-              $unset: ['supportedCommandsForBroadcastUse', 'supportedCommandsForSingleUse'],
+            $unset: {
+              supportedCommandsForBroadcastUse: '',
+              supportedCommandsForSingleUse: '',
             },
             },
-          ],
+          },
         },
         },
       };
       };
     });
     });
 
 
-    await SlackAppIntegration.bulkWrite(operations);
+    await db.collection('slackappintegrations').bulkWrite(operations);
 
 
     logger.info('Migration has successfully applied');
     logger.info('Migration has successfully applied');
   },
   },
 
 
   async down(db, next) {
   async down(db, next) {
     logger.info('Rollback migration');
     logger.info('Rollback migration');
-    // return next();
-    mongoose.connect(config.mongoUri, config.mongodb.options);
+    await mongoose.connect(getMongoUri(), mongoOptions);
 
 
     const SlackAppIntegration = getModelSafely('SlackAppIntegration') || require('~/server/models/slack-app-integration')();
     const SlackAppIntegration = getModelSafely('SlackAppIntegration') || require('~/server/models/slack-app-integration')();
 
 
     const slackAppIntegrations = await SlackAppIntegration.find();
     const slackAppIntegrations = await SlackAppIntegration.find();
 
 
+    if (slackAppIntegrations.length === 0) return next();
+
     // create operations
     // create operations
     const operations = slackAppIntegrations.map((doc) => {
     const operations = slackAppIntegrations.map((doc) => {
       const dataForBroadcastUse = [];
       const dataForBroadcastUse = [];
@@ -100,22 +115,21 @@ module.exports = {
       return {
       return {
         updateOne: {
         updateOne: {
           filter: { _id: doc._id },
           filter: { _id: doc._id },
-          update: [
-            {
-              $set: {
-                supportedCommandsForBroadcastUse: dataForBroadcastUse,
-                supportedCommandsForSingleUse: dataForSingleUse,
-              },
+          update: {
+            $set: {
+              supportedCommandsForBroadcastUse: dataForBroadcastUse,
+              supportedCommandsForSingleUse: dataForSingleUse,
             },
             },
-            {
-              $unset: ['permissionsForBroadcastUseCommands', 'permissionsForSingleUseCommands'],
+            $unset: {
+              permissionsForBroadcastUseCommands: '',
+              permissionsForSingleUseCommands: '',
             },
             },
-          ],
+          },
         },
         },
       };
       };
     });
     });
 
 
-    await SlackAppIntegration.bulkWrite(operations);
+    await db.collection('slackappintegrations').bulkWrite(operations);
 
 
     next();
     next();
     logger.info('Migration has successfully applied');
     logger.info('Migration has successfully applied');

+ 1 - 1
packages/app/src/server/console.js

@@ -2,7 +2,7 @@ const repl = require('repl');
 const fs = require('fs');
 const fs = require('fs');
 const path = require('path');
 const path = require('path');
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
-const { initMongooseGlobalSettings, getMongoUri, mongoOptions } = require('~/server/util/mongoose-utils');
+const { initMongooseGlobalSettings, getMongoUri, mongoOptions } = require('@growi/core');
 
 
 const models = require('./models');
 const models = require('./models');
 
 

+ 28 - 5
packages/app/src/server/crowi/index.js

@@ -1,15 +1,18 @@
 /* eslint-disable @typescript-eslint/no-this-alias */
 /* eslint-disable @typescript-eslint/no-this-alias */
 
 
 import path from 'path';
 import path from 'path';
+import http from 'http';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
+import { createTerminus } from '@godaddy/terminus';
+
+import { initMongooseGlobalSettings, getMongoUri, mongoOptions } from '@growi/core';
 import pkg from '^/package.json';
 import pkg from '^/package.json';
 
 
 import CdnResourcesService from '~/services/cdn-resources-service';
 import CdnResourcesService from '~/services/cdn-resources-service';
 import InterceptorManager from '~/services/interceptor-manager';
 import InterceptorManager from '~/services/interceptor-manager';
 import Xss from '~/services/xss';
 import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { initMongooseGlobalSettings, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import { projectRoot } from '~/utils/project-dir-utils';
 import { projectRoot } from '~/utils/project-dir-utils';
 
 
 import ConfigManager from '../service/config-manager';
 import ConfigManager from '../service/config-manager';
@@ -409,10 +412,17 @@ Crowi.prototype.start = async function() {
   this.pluginService = new PluginService(this, express);
   this.pluginService = new PluginService(this, express);
   await this.pluginService.autoDetectAndLoadPlugins();
   await this.pluginService.autoDetectAndLoadPlugins();
 
 
-  const server = (this.node_env === 'development') ? this.crowiDev.setupServer(express) : express;
+  const app = (this.node_env === 'development') ? this.crowiDev.setupServer(express) : express;
+
+  const httpServer = http.createServer(app);
+
+  // setup terminus
+  this.setupTerminus(httpServer);
+  // attach to socket.io
+  this.socketIoService.attachServer(httpServer);
 
 
   // listen
   // listen
-  const serverListening = server.listen(this.port, () => {
+  const serverListening = httpServer.listen(this.port, () => {
     logger.info(`[${this.node_env}] Express server is listening on port ${this.port}`);
     logger.info(`[${this.node_env}] Express server is listening on port ${this.port}`);
     if (this.node_env === 'development') {
     if (this.node_env === 'development') {
       this.crowiDev.setupExpressAfterListening(express);
       this.crowiDev.setupExpressAfterListening(express);
@@ -428,8 +438,6 @@ Crowi.prototype.start = async function() {
     });
     });
   }
   }
 
 
-  this.socketIoService.attachServer(serverListening);
-
   // setup Express Routes
   // setup Express Routes
   this.setupRoutesAtLast();
   this.setupRoutesAtLast();
 
 
@@ -463,6 +471,21 @@ Crowi.prototype.buildServer = async function() {
   this.express = express;
   this.express = express;
 };
 };
 
 
+Crowi.prototype.setupTerminus = function(server) {
+  createTerminus(server, {
+    signals: ['SIGINT', 'SIGTERM'],
+    onSignal: async() => {
+      logger.info('Server is starting cleanup');
+
+      await mongoose.disconnect();
+      return;
+    },
+    onShutdown: async() => {
+      logger.info('Cleanup finished, server is shutting down');
+    },
+  });
+};
+
 /**
 /**
  * setup Express Routes
  * setup Express Routes
  * !! this must be at last because it includes '/*' route !!
  * !! this must be at last because it includes '/*' route !!

+ 1 - 1
packages/app/src/server/models/config.ts

@@ -1,7 +1,7 @@
 import { Types, Schema } from 'mongoose';
 import { Types, Schema } from 'mongoose';
 import uniqueValidator from 'mongoose-unique-validator';
 import uniqueValidator from 'mongoose-unique-validator';
 
 
-import { getOrCreateModel } from '../util/mongoose-utils';
+import { getOrCreateModel } from '@growi/core';
 
 
 export interface Config {
 export interface Config {
   _id: Types.ObjectId;
   _id: Types.ObjectId;

+ 1 - 1
packages/app/src/server/models/editor-settings.ts

@@ -1,7 +1,7 @@
 import {
 import {
   Schema, Model, Document,
   Schema, Model, Document,
 } from 'mongoose';
 } from 'mongoose';
-import { getOrCreateModel } from '../util/mongoose-utils';
+import { getOrCreateModel } from '@growi/core';
 
 
 
 
 export interface ILintRule {
 export interface ILintRule {

+ 1 - 1
packages/app/src/server/models/password-reset-order.ts

@@ -4,7 +4,7 @@ import mongoose, {
 
 
 import uniqueValidator from 'mongoose-unique-validator';
 import uniqueValidator from 'mongoose-unique-validator';
 import crypto from 'crypto';
 import crypto from 'crypto';
-import { getOrCreateModel } from '../util/mongoose-utils';
+import { getOrCreateModel } from '@growi/core';
 
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
 

+ 1 - 1
packages/app/src/server/models/update-post.ts

@@ -3,7 +3,7 @@
 import {
 import {
   Types, Schema, Model, Document,
   Types, Schema, Model, Document,
 } from 'mongoose';
 } from 'mongoose';
-import { getOrCreateModel } from '../util/mongoose-utils';
+import { getOrCreateModel } from '@growi/core';
 
 
 export interface IUpdatePost {
 export interface IUpdatePost {
   pathPattern: string
   pathPattern: string

+ 2 - 3
packages/app/src/server/routes/apiv3/slack-integration-settings.js

@@ -164,7 +164,7 @@ module.exports = (crowi) => {
    */
    */
   router.get('/', accessTokenParser, loginRequiredStrictly, adminRequired, async(req, res) => {
   router.get('/', accessTokenParser, loginRequiredStrictly, adminRequired, async(req, res) => {
 
 
-    const { configManager } = crowi;
+    const { configManager, slackIntegrationService } = crowi;
     const currentBotType = configManager.getConfig('crowi', 'slackbot:currentBotType');
     const currentBotType = configManager.getConfig('crowi', 'slackbot:currentBotType');
 
 
     // retrieve settings
     // retrieve settings
@@ -177,8 +177,7 @@ module.exports = (crowi) => {
       settings.commandPermission = JSON.parse(configManager.getConfig('crowi', 'slackbot:withoutProxy:commandPermission'));
       settings.commandPermission = JSON.parse(configManager.getConfig('crowi', 'slackbot:withoutProxy:commandPermission'));
     }
     }
     else {
     else {
-      settings.proxyServerUri = crowi.configManager.getConfig('crowi', 'slackbot:proxyUri');
-      settings.proxyUriEnvVars = configManager.getConfigFromEnvVars('crowi', 'slackbot:proxyUri');
+      settings.proxyServerUri = slackIntegrationService.proxyUriForCurrentType;
     }
     }
 
 
     // retrieve connection statuses
     // retrieve connection statuses

+ 151 - 244
packages/app/src/server/service/slack-command-handler/search.js

@@ -3,33 +3,68 @@ import loggerFactory from '~/utils/logger';
 const logger = loggerFactory('growi:service:SlackCommandHandler:search');
 const logger = loggerFactory('growi:service:SlackCommandHandler:search');
 
 
 const {
 const {
-  markdownSectionBlock, divider, respond, deleteOriginal,
+  markdownSectionBlock, divider, respond, respondInChannel, replaceOriginal, deleteOriginal,
 } = require('@growi/slack');
 } = require('@growi/slack');
 const { formatDistanceStrict } = require('date-fns');
 const { formatDistanceStrict } = require('date-fns');
 const SlackbotError = require('../../models/vo/slackbot-error');
 const SlackbotError = require('../../models/vo/slackbot-error');
 
 
-const PAGINGLIMIT = 10;
+const PAGINGLIMIT = 7;
+
 
 
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const BaseSlackCommandHandler = require('./slack-command-handler');
   const BaseSlackCommandHandler = require('./slack-command-handler');
   const handler = new BaseSlackCommandHandler(crowi);
   const handler = new BaseSlackCommandHandler(crowi);
 
 
-  handler.handleCommand = async function(growiCommand, client, body) {
-    const { responseUrl, growiCommandArgs } = growiCommand;
-    let searchResult;
-    try {
-      searchResult = await this.retrieveSearchResults(responseUrl, client, body, growiCommandArgs);
-    }
-    catch (err) {
-      logger.error('Failed to get search results.', err);
-      throw new SlackbotError({
-        method: 'postEphemeral',
-        to: 'channel',
-        popupMessage: 'Failed To Search',
-        mainMessage: '*Failed to search.*\n Hint\n `/growi search [keyword]`',
-      });
+
+  function getKeywords(growiCommandArgs) {
+    const keywords = growiCommandArgs.join(' ');
+    return keywords;
+  }
+
+  function appendSpeechBaloon(mrkdwn, commentCount) {
+    return (commentCount != null && commentCount > 0)
+      ? `${mrkdwn}   :speech_balloon: ${commentCount}`
+      : mrkdwn;
+  }
+
+  function generateSearchResultPageLinkMrkdwn(appUrl, growiCommandArgs) {
+    const url = new URL('/_search', appUrl);
+    url.searchParams.append('q', growiCommandArgs.map(kwd => encodeURIComponent(kwd)).join('+'));
+    return `<${url.href} | Results page>`;
+  }
+
+  function generatePageLinkMrkdwn(pathname, href) {
+    return `<${decodeURI(href)} | ${decodeURI(pathname)}>`;
+  }
+
+  function generateLastUpdateMrkdwn(updatedAt, baseDate) {
+    if (updatedAt != null) {
+      // cast to date
+      const date = new Date(updatedAt);
+      return formatDistanceStrict(date, baseDate);
     }
     }
+    return '';
+  }
+
+  async function retrieveSearchResults(growiCommandArgs, offset = 0) {
+    const keywords = getKeywords(growiCommandArgs);
+
+    const { searchService } = crowi;
+    const options = { limit: PAGINGLIMIT, offset };
+    const results = await searchService.searchKeyword(keywords, null, {}, options);
+    const resultsTotal = results.meta.total;
 
 
+    const pages = results.data.map((data) => {
+      const { path, updated_at: updatedAt, comment_count: commentCount } = data._source;
+      return { path, updatedAt, commentCount };
+    });
+
+    return {
+      pages, offset, resultsTotal,
+    };
+  }
+
+  function buildRespondBodyForSearchResult(searchResult, growiCommandArgs) {
     const appUrl = crowi.appService.getSiteUrl();
     const appUrl = crowi.appService.getSiteUrl();
     const appTitle = crowi.appService.getAppTitle();
     const appTitle = crowi.appService.getAppTitle();
 
 
@@ -37,22 +72,9 @@ module.exports = (crowi) => {
       pages, offset, resultsTotal,
       pages, offset, resultsTotal,
     } = searchResult;
     } = searchResult;
 
 
-    const keywords = this.getKeywords(growiCommandArgs);
-
+    const keywords = getKeywords(growiCommandArgs);
 
 
     let searchResultsDesc;
     let searchResultsDesc;
-
-    if (resultsTotal === 0 || resultsTotal == null) {
-      if (keywords === '') return;
-      await respond(responseUrl, {
-        text: 'No page found.',
-        blocks: [
-          markdownSectionBlock(`No page found. keyword(s): *"${keywords}"*`),
-          markdownSectionBlock('Please try other keywords.'),
-        ],
-      });
-      return;
-    }
     switch (resultsTotal) {
     switch (resultsTotal) {
       case 1:
       case 1:
         searchResultsDesc = `*${resultsTotal}* page is found.`;
         searchResultsDesc = `*${resultsTotal}* page is found.`;
@@ -62,13 +84,15 @@ module.exports = (crowi) => {
         break;
         break;
     }
     }
 
 
-
     const contextBlock = {
     const contextBlock = {
       type: 'context',
       type: 'context',
       elements: [
       elements: [
         {
         {
           type: 'mrkdwn',
           type: 'mrkdwn',
-          text: `keyword(s) : *"${keywords}"*  |  Current: ${offset + 1} - ${offset + pages.length}  |  Total ${resultsTotal} pages`,
+          text: `keyword(s) : *"${keywords}"*`
+          + `  |  Total ${resultsTotal} pages`
+          + `  |  Current: ${offset + 1} - ${offset + pages.length}`
+          + `  |  ${generateSearchResultPageLinkMrkdwn(appUrl, growiCommandArgs)}`,
         },
         },
       ],
       ],
     };
     };
@@ -89,8 +113,8 @@ module.exports = (crowi) => {
           type: 'section',
           type: 'section',
           text: {
           text: {
             type: 'mrkdwn',
             type: 'mrkdwn',
-            text: `${this.appendSpeechBaloon(`*${this.generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`
-              + `\n    Last updated: ${this.generateLastUpdateMrkdwn(updatedAt, now)}`,
+            text: `${appendSpeechBaloon(`*${generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`
+              + `  \`${generateLastUpdateMrkdwn(updatedAt, now)}\``,
           },
           },
           accessory: {
           accessory: {
             type: 'button',
             type: 'button',
@@ -121,26 +145,96 @@ module.exports = (crowi) => {
         },
         },
       ],
       ],
     };
     };
+    // show "Prev" button if previous page exists
+    // eslint-disable-next-line yoda
+    if (0 < offset) {
+      actionBlocks.elements.unshift(
+        {
+          type: 'button',
+          text: {
+            type: 'plain_text',
+            text: '< Prev',
+          },
+          action_id: 'search:showPrevResults',
+          value: JSON.stringify({ offset, growiCommandArgs }),
+        },
+      );
+    }
     // show "Next" button if next page exists
     // show "Next" button if next page exists
-    if (resultsTotal > offset + PAGINGLIMIT) {
+    if (offset + PAGINGLIMIT < resultsTotal) {
       actionBlocks.elements.unshift(
       actionBlocks.elements.unshift(
         {
         {
           type: 'button',
           type: 'button',
           text: {
           text: {
             type: 'plain_text',
             type: 'plain_text',
-            text: 'Next',
+            text: 'Next >',
           },
           },
           action_id: 'search:showNextResults',
           action_id: 'search:showNextResults',
-          value: JSON.stringify({ offset, body, growiCommandArgs }),
+          value: JSON.stringify({ offset, growiCommandArgs }),
         },
         },
       );
       );
     }
     }
     blocks.push(actionBlocks);
     blocks.push(actionBlocks);
 
 
-    await respond(responseUrl, {
+    return {
       text: 'Successed To Search',
       text: 'Successed To Search',
       blocks,
       blocks,
-    });
+    };
+  }
+
+
+  async function buildRespondBody(growiCommandArgs) {
+    const firstKeyword = growiCommandArgs[0];
+
+    // enpty keyword
+    if (firstKeyword == null) {
+      return {
+        text: 'Input keywords',
+        blocks: [
+          markdownSectionBlock('*Input keywords.*\n Hint\n `/growi search [keyword]`'),
+        ],
+      };
+    }
+
+    const searchResult = await retrieveSearchResults(growiCommandArgs);
+
+    // no search results
+    if (searchResult.resultsTotal === 0) {
+      const keywords = getKeywords(growiCommandArgs);
+      logger.info(`No page found with "${keywords}"`);
+
+      return {
+        text: `No page found with "${keywords}"`,
+        blocks: [
+          markdownSectionBlock(`*No page matches your keyword(s) "${keywords}".*`),
+          markdownSectionBlock(':mag: *Help: Searching*'),
+          divider(),
+          markdownSectionBlock('`word1` `word2` (divide with space) \n Search pages that include both word1, word2 in the title or body'),
+          divider(),
+          markdownSectionBlock('`"This is GROWI"` (surround with double quotes) \n Search pages that include the phrase "This is GROWI"'),
+          divider(),
+          markdownSectionBlock('`-keyword` \n Exclude pages that include keyword in the title or body'),
+          divider(),
+          markdownSectionBlock('`prefix:/user/` \n Search only the pages that the title start with /user/'),
+          divider(),
+          markdownSectionBlock('`-prefix:/user/` \n Exclude the pages that the title start with /user/'),
+          divider(),
+          markdownSectionBlock('`tag:wiki` \n Search for pages with wiki tag'),
+          divider(),
+          markdownSectionBlock('`-tag:wiki` \n Exclude pages with wiki tag'),
+        ],
+      };
+    }
+
+    return buildRespondBodyForSearchResult(searchResult, growiCommandArgs);
+  }
+
+
+  handler.handleCommand = async function(growiCommand, client, body) {
+    const { responseUrl, growiCommandArgs } = growiCommand;
+
+    const respondBody = await buildRespondBody(growiCommandArgs);
+    await respond(responseUrl, respondBody);
   };
   };
 
 
   handler.handleInteractions = async function(client, interactionPayload, interactionPayloadAccessor, handlerMethodName) {
   handler.handleInteractions = async function(client, interactionPayload, interactionPayloadAccessor, handlerMethodName) {
@@ -171,16 +265,18 @@ module.exports = (crowi) => {
 
 
     // share
     // share
     const now = new Date();
     const now = new Date();
-    return respond(responseUrl, {
+    return respondInChannel(responseUrl, {
       blocks: [
       blocks: [
         { type: 'divider' },
         { type: 'divider' },
-        markdownSectionBlock(`${this.appendSpeechBaloon(`*${this.generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`),
+        markdownSectionBlock(`${appendSpeechBaloon(`*${generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`),
         {
         {
           type: 'context',
           type: 'context',
           elements: [
           elements: [
             {
             {
               type: 'mrkdwn',
               type: 'mrkdwn',
-              text: `<${decodeURI(appUrl)}|*${appTitle}*>  |  Last updated: ${this.generateLastUpdateMrkdwn(updatedAt, now)}  |  Shared by *${user.username}*`,
+              text: `<${decodeURI(appUrl)}|*${appTitle}*>`
+                + `  |  Last updated: \`${generateLastUpdateMrkdwn(updatedAt, now)}\``
+                + `  |  Shared by *${user.username}*`,
             },
             },
           ],
           ],
         },
         },
@@ -188,7 +284,7 @@ module.exports = (crowi) => {
     });
     });
   };
   };
 
 
-  handler.showNextResults = async function(client, payload, interactionPayloadAccessor) {
+  async function showPrevOrNextResults(interactionPayloadAccessor, isNext = true) {
     const responseUrl = interactionPayloadAccessor.getResponseUrl();
     const responseUrl = interactionPayloadAccessor.getResponseUrl();
 
 
     const value = interactionPayloadAccessor.firstAction()?.value;
     const value = interactionPayloadAccessor.firstAction()?.value;
@@ -203,131 +299,22 @@ module.exports = (crowi) => {
     }
     }
     const parsedValue = JSON.parse(value);
     const parsedValue = JSON.parse(value);
 
 
-    const { body, growiCommandArgs, offset: offsetNum } = parsedValue;
-    const newOffsetNum = offsetNum + 10;
-    let searchResult;
-    try {
-      searchResult = await this.retrieveSearchResults(responseUrl, client, body, growiCommandArgs, newOffsetNum);
-    }
-    catch (err) {
-      logger.error('Failed to get search results.', err);
-      throw new SlackbotError({
-        method: 'postEphemeral',
-        to: 'channel',
-        popupMessage: 'Failed To Search',
-        mainMessage: '*Failed to search.*\n Hint\n `/growi search [keyword]`',
-      });
-    }
+    const { growiCommandArgs, offset: offsetNum } = parsedValue;
+    const newOffsetNum = isNext
+      ? offsetNum + PAGINGLIMIT
+      : offsetNum - PAGINGLIMIT;
 
 
-    const appUrl = crowi.appService.getSiteUrl();
-    const appTitle = crowi.appService.getAppTitle();
+    const searchResult = await retrieveSearchResults(growiCommandArgs, newOffsetNum);
 
 
-    const {
-      pages, offset, resultsTotal,
-    } = searchResult;
-
-    const keywords = this.getKeywords(growiCommandArgs);
-
-
-    let searchResultsDesc;
-
-    if (resultsTotal === 0 || resultsTotal == null) {
-      if (keywords === '') return;
-      await respond(responseUrl, {
-        text: 'No page found.',
-        blocks: [
-          markdownSectionBlock('Please try with other keywords.'),
-        ],
-      });
-      return;
-    }
-    switch (resultsTotal) {
-      case 1:
-        searchResultsDesc = `*${resultsTotal}* page is found.`;
-        break;
-      default:
-        searchResultsDesc = `*${resultsTotal}* pages are found.`;
-        break;
-    }
-
-    const contextBlock = {
-      type: 'context',
-      elements: [
-        {
-          type: 'mrkdwn',
-          text: `keyword(s) : *"${keywords}"*  |  Current: ${offset + 1} - ${offset + pages.length}  |  Total ${resultsTotal} pages`,
-        },
-      ],
-    };
+    await replaceOriginal(responseUrl, buildRespondBodyForSearchResult(searchResult, growiCommandArgs));
+  }
 
 
-    const now = new Date();
-    const blocks = [
-      markdownSectionBlock(`:mag: <${decodeURI(appUrl)}|*${appTitle}*>\n${searchResultsDesc}`),
-      contextBlock,
-      { type: 'divider' },
-      // create an array by map and extract
-      ...pages.map((page) => {
-        const { path, updatedAt, commentCount } = page;
-        // generate URL
-        const url = new URL(path, appUrl);
-        const { href, pathname } = url;
-
-        return {
-          type: 'section',
-          text: {
-            type: 'mrkdwn',
-            text: `${this.appendSpeechBaloon(`*${this.generatePageLinkMrkdwn(pathname, href)}*`, commentCount)}`
-              + `\n    Last updated: ${this.generateLastUpdateMrkdwn(updatedAt, now)}`,
-          },
-          accessory: {
-            type: 'button',
-            action_id: 'search:shareSinglePageResult',
-            text: {
-              type: 'plain_text',
-              text: 'Share',
-            },
-            value: JSON.stringify({ page, href, pathname }),
-          },
-        };
-      }),
-      { type: 'divider' },
-      contextBlock,
-    ];
-
-    const actionBlocks = {
-      type: 'actions',
-      elements: [
-        {
-          type: 'button',
-          text: {
-            type: 'plain_text',
-            text: 'Dismiss',
-          },
-          style: 'danger',
-          action_id: 'search:dismissSearchResults',
-        },
-      ],
-    };
-    // show "Next" button if next page exists
-    if (resultsTotal > offset + PAGINGLIMIT) {
-      actionBlocks.elements.unshift(
-        {
-          type: 'button',
-          text: {
-            type: 'plain_text',
-            text: 'Next',
-          },
-          action_id: 'search:showNextResults',
-          value: JSON.stringify({ offset, body, growiCommandArgs }),
-        },
-      );
-    }
-    blocks.push(actionBlocks);
+  handler.showPrevResults = async function(client, payload, interactionPayloadAccessor) {
+    return showPrevOrNextResults(interactionPayloadAccessor, false);
+  };
 
 
-    await respond(responseUrl, {
-      text: 'Successed To Search',
-      blocks,
-    });
+  handler.showNextResults = async function(client, payload, interactionPayloadAccessor) {
+    return showPrevOrNextResults(interactionPayloadAccessor, true);
   };
   };
 
 
   handler.dismissSearchResults = async function(client, payload) {
   handler.dismissSearchResults = async function(client, payload) {
@@ -338,85 +325,5 @@ module.exports = (crowi) => {
     });
     });
   };
   };
 
 
-  handler.retrieveSearchResults = async function(responseUrl, client, body, growiCommandArgs, offset = 0) {
-    const firstKeyword = growiCommandArgs[0];
-    if (firstKeyword == null) {
-      await respond(responseUrl, {
-        text: 'Input keywords',
-        blocks: [
-          markdownSectionBlock('*Input keywords.*\n Hint\n `/growi search [keyword]`'),
-        ],
-      });
-      return { pages: [] };
-    }
-
-    const keywords = this.getKeywords(growiCommandArgs);
-
-    const { searchService } = crowi;
-    const options = { limit: 10, offset };
-    const results = await searchService.searchKeyword(keywords, null, {}, options);
-    const resultsTotal = results.meta.total;
-
-    // no search results
-    if (results.data.length === 0) {
-      logger.info(`No page found with "${keywords}"`);
-      await respond(responseUrl, {
-        text: `No page found with "${keywords}"`,
-        blocks: [
-          markdownSectionBlock(`*No page matches your keyword(s) "${keywords}".*`),
-          markdownSectionBlock(':mag: *Help: Searching*'),
-          divider(),
-          markdownSectionBlock('`word1` `word2` (divide with space) \n Search pages that include both word1, word2 in the title or body'),
-          divider(),
-          markdownSectionBlock('`"This is GROWI"` (surround with double quotes) \n Search pages that include the phrase "This is GROWI"'),
-          divider(),
-          markdownSectionBlock('`-keyword` \n Exclude pages that include keyword in the title or body'),
-          divider(),
-          markdownSectionBlock('`prefix:/user/` \n Search only the pages that the title start with /user/'),
-          divider(),
-          markdownSectionBlock('`-prefix:/user/` \n Exclude the pages that the title start with /user/'),
-          divider(),
-          markdownSectionBlock('`tag:wiki` \n Search for pages with wiki tag'),
-          divider(),
-          markdownSectionBlock('`-tag:wiki` \n Exclude pages with wiki tag'),
-        ],
-      });
-      return { pages: [] };
-    }
-
-    const pages = results.data.map((data) => {
-      const { path, updated_at: updatedAt, comment_count: commentCount } = data._source;
-      return { path, updatedAt, commentCount };
-    });
-
-    return {
-      pages, offset, resultsTotal,
-    };
-  };
-
-  handler.getKeywords = function(growiCommandArgs) {
-    const keywords = growiCommandArgs.join(' ');
-    return keywords;
-  };
-
-  handler.appendSpeechBaloon = function(mrkdwn, commentCount) {
-    return (commentCount != null && commentCount > 0)
-      ? `${mrkdwn}   :speech_balloon: ${commentCount}`
-      : mrkdwn;
-  };
-
-  handler.generatePageLinkMrkdwn = function(pathname, href) {
-    return `<${decodeURI(href)} | ${decodeURI(pathname)}>`;
-  };
-
-  handler.generateLastUpdateMrkdwn = function(updatedAt, baseDate) {
-    if (updatedAt != null) {
-      // cast to date
-      const date = new Date(updatedAt);
-      return formatDistanceStrict(date, baseDate);
-    }
-    return '';
-  };
-
   return handler;
   return handler;
 };
 };

+ 28 - 6
packages/app/src/server/service/slack-integration.ts

@@ -237,17 +237,25 @@ export class SlackIntegrationService implements S2sMessageHandlable {
   /**
   /**
    * Handle /commands endpoint
    * Handle /commands endpoint
    */
    */
-  async handleCommandRequest(growiCommand: GrowiCommand, client, body) {
+  async handleCommandRequest(growiCommand: GrowiCommand, client, body): Promise<void> {
     const { growiCommandType } = growiCommand;
     const { growiCommandType } = growiCommand;
     const module = `./slack-command-handler/${growiCommandType}`;
     const module = `./slack-command-handler/${growiCommandType}`;
 
 
+    let handler;
     try {
     try {
-      const handler = require(module)(this.crowi);
-      await handler.handleCommand(growiCommand, client, body);
+      handler = require(module)(this.crowi);
     }
     }
     catch (err) {
     catch (err) {
+      logger.error(err);
       await this.notCommand(growiCommand);
       await this.notCommand(growiCommand);
-      throw err;
+    }
+
+    try {
+      await handler.handleCommand(growiCommand, client, body);
+    }
+    catch (err) {
+      logger.error(err);
+      await this.notifyInternalError(growiCommand.responseUrl, err);
     }
     }
   }
   }
 
 
@@ -261,7 +269,9 @@ export class SlackIntegrationService implements S2sMessageHandlable {
       await handler.handleInteractions(client, interactionPayload, interactionPayloadAccessor, handlerMethodName);
       await handler.handleInteractions(client, interactionPayload, interactionPayloadAccessor, handlerMethodName);
     }
     }
     catch (err) {
     catch (err) {
-      throw err;
+      logger.error(err);
+      const responseUrl = interactionPayloadAccessor.getResponseUrl();
+      await this.notifyInternalError(responseUrl, err);
     }
     }
     return;
     return;
   }
   }
@@ -276,7 +286,9 @@ export class SlackIntegrationService implements S2sMessageHandlable {
       await handler.handleInteractions(client, interactionPayload, interactionPayloadAccessor, handlerMethodName);
       await handler.handleInteractions(client, interactionPayload, interactionPayloadAccessor, handlerMethodName);
     }
     }
     catch (err) {
     catch (err) {
-      throw err;
+      logger.error(err);
+      const responseUrl = interactionPayloadAccessor.getResponseUrl();
+      await this.notifyInternalError(responseUrl, err);
     }
     }
     return;
     return;
   }
   }
@@ -292,4 +304,14 @@ export class SlackIntegrationService implements S2sMessageHandlable {
     return;
     return;
   }
   }
 
 
+  async notifyInternalError(responseUrl: string, error: Error): Promise<void> {
+    await respond(responseUrl, {
+      text: 'Internal Server Error',
+      blocks: [
+        markdownSectionBlock(`*Internal Server Error*\n \`${error.message}\``),
+      ],
+    });
+    return;
+  }
+
 }
 }

+ 5 - 2
packages/app/src/server/service/socket-io.js

@@ -1,7 +1,8 @@
+import { Server } from 'socket.io';
+
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { RoomPrefix, getRoomNameWithId } from '../util/socket-io-helpers';
 import { RoomPrefix, getRoomNameWithId } from '../util/socket-io-helpers';
 
 
-const socketIo = require('socket.io');
 const expressSession = require('express-session');
 const expressSession = require('express-session');
 const passport = require('passport');
 const passport = require('passport');
 
 
@@ -26,9 +27,11 @@ class SocketIoService {
 
 
   // Since the Order is important, attachServer() should be async
   // Since the Order is important, attachServer() should be async
   async attachServer(server) {
   async attachServer(server) {
-    this.io = socketIo(server, {
+    this.io = new Server({
       transports: ['websocket'],
       transports: ['websocket'],
+      serveClient: false,
     });
     });
+    this.io.attach(server);
 
 
     // create namespace for admin
     // create namespace for admin
     this.adminNamespace = this.io.of('/admin');
     this.adminNamespace = this.io.of('/admin');

+ 0 - 40
packages/app/src/test/config/migrate.test.js

@@ -1,40 +0,0 @@
-describe('config/migrate.js', () => {
-
-  beforeEach(async() => {
-    jest.resetModules();
-  });
-
-  /* eslint-disable indent */
-  describe.each`
-    MONGO_URI                                         | expectedUrl                                       | expectedDbName
-    ${'mongodb://example.com/growi'}                  | ${'mongodb://example.com/growi'}                  | ${'growi'}
-    ${'mongodb://user:pass@example.com/growi'}        | ${'mongodb://user:pass@example.com/growi'}        | ${'growi'}
-    ${'mongodb://example.com/growi?replicaSet=mySet'} | ${'mongodb://example.com/growi?replicaSet=mySet'} | ${'growi'}
-  `('returns', ({ MONGO_URI, expectedUrl, expectedDbName }) => {
-    test(`when 'MONGO_URI' is '${MONGO_URI}`, () => {
-
-      const initMongooseGlobalSettingsMock = jest.fn();
-
-      // mock for mongoose-utils
-      jest.doMock('~/server/util/mongoose-utils', () => {
-        return {
-          initMongooseGlobalSettings: initMongooseGlobalSettingsMock,
-          getMongoUri: () => {
-            return MONGO_URI;
-          },
-        };
-      });
-
-      const { mongoUri, mongodb } = require('^/config/migrate');
-
-      jest.dontMock('~/server/util/mongoose-utils');
-
-      expect(initMongooseGlobalSettingsMock).toHaveBeenCalledTimes(1);
-      expect(mongoUri).toBe(MONGO_URI);
-      expect(mongodb.url).toBe(expectedUrl);
-      expect(mongodb.databaseName).toBe(expectedDbName);
-    });
-  });
-  /* eslint-enable indent */
-
-});

+ 0 - 0
packages/app/src/test/crowi/crowi.test.js → packages/app/src/test/integration/crowi/crowi.test.js


+ 1 - 1
packages/app/src/test/global-setup.js → packages/app/src/test/integration/global-setup.js

@@ -9,7 +9,7 @@ import 'tsconfig-paths/register';
 
 
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
-import { initMongooseGlobalSettings, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import { initMongooseGlobalSettings, getMongoUri, mongoOptions } from '@growi/core';
 
 
 // check env
 // check env
 if (process.env.NODE_ENV !== 'test') {
 if (process.env.NODE_ENV !== 'test') {

+ 0 - 0
packages/app/src/test/global-teardown.js → packages/app/src/test/integration/global-teardown.js


+ 0 - 0
packages/app/src/test/middlewares/access-token-parser.test.js → packages/app/src/test/integration/middlewares/access-token-parser.test.js


+ 0 - 0
packages/app/src/test/middlewares/login-required.test.js → packages/app/src/test/integration/middlewares/login-required.test.js


+ 122 - 0
packages/app/src/test/integration/migrations/20210913153942-migrate-slack-app-integration-schema.test.ts

@@ -0,0 +1,122 @@
+import mongoose from 'mongoose';
+import { Collection } from 'mongodb';
+import { getMongoUri, mongoOptions } from '@growi/core';
+
+const migrate = require('../../../migrations/20210913153942-migrate-slack-app-integration-schema');
+
+describe('migrate-slack-app-integration-schema', () => {
+
+  let collection: Collection;
+
+  beforeAll(async() => {
+    await mongoose.connect(getMongoUri(), mongoOptions);
+    collection = mongoose.connection.db.collection('slackappintegrations');
+
+    await collection.insertMany([
+      {
+        tokenGtoP: 'tokenGtoP1', tokenPtoG: 'tokenPtoG1', permissionsForBroadcastUseCommands: { foo: true }, permissionsForSingleUseCommands: { bar: true },
+      },
+      {
+        tokenGtoP: 'tokenGtoP2', tokenPtoG: 'tokenPtoG2', supportedCommandsForBroadcastUse: ['foo'], supportedCommandsForSingleUse: ['bar'],
+      },
+      {
+        tokenGtoP: 'tokenGtoP3', tokenPtoG: 'tokenPtoG3',
+      },
+    ]);
+  });
+
+  test('up is applied successfully', async() => {
+    // setup
+    const doc1 = await collection.findOne({ tokenGtoP: 'tokenGtoP1' });
+    const doc2 = await collection.findOne({ tokenGtoP: 'tokenGtoP2' });
+    const doc3 = await collection.findOne({ tokenGtoP: 'tokenGtoP3' });
+    expect(doc1 != null).toBeTruthy();
+    expect(doc2 != null).toBeTruthy();
+    expect(doc3 != null).toBeTruthy();
+    expect(doc1).toStrictEqual({
+      _id: doc1._id,
+      tokenGtoP: 'tokenGtoP1',
+      tokenPtoG: 'tokenPtoG1',
+      permissionsForBroadcastUseCommands: {
+        foo: true,
+      },
+      permissionsForSingleUseCommands: {
+        bar: true,
+      },
+    });
+    expect(doc2).toStrictEqual({
+      _id: doc2._id,
+      tokenGtoP: 'tokenGtoP2',
+      tokenPtoG: 'tokenPtoG2',
+      supportedCommandsForBroadcastUse: [
+        'foo',
+      ],
+      supportedCommandsForSingleUse: [
+        'bar',
+      ],
+    });
+    expect(doc3).toStrictEqual({
+      _id: doc3._id,
+      tokenGtoP: 'tokenGtoP3',
+      tokenPtoG: 'tokenPtoG3',
+    });
+
+    // when
+    await migrate.up(mongoose.connection.db);
+
+    // then
+    const fixedDoc1 = await collection.findOne({ tokenGtoP: 'tokenGtoP1' });
+    const fixedDoc2 = await collection.findOne({ tokenGtoP: 'tokenGtoP2' });
+    const fixedDoc3 = await collection.findOne({ tokenGtoP: 'tokenGtoP3' });
+    expect(fixedDoc1 != null).toBeTruthy();
+    expect(fixedDoc2 != null).toBeTruthy();
+    expect(fixedDoc3 != null).toBeTruthy();
+    expect(fixedDoc1.supportedCommandsForBroadcastUse).toBeUndefined();
+    expect(fixedDoc1.supportedCommandsForSingleUse).toBeUndefined();
+    expect(fixedDoc2.supportedCommandsForBroadcastUse).toBeUndefined();
+    expect(fixedDoc2.supportedCommandsForSingleUse).toBeUndefined();
+    expect(fixedDoc3.supportedCommandsForBroadcastUse).toBeUndefined();
+    expect(fixedDoc3.supportedCommandsForSingleUse).toBeUndefined();
+    expect(fixedDoc1).toStrictEqual({
+      _id: doc1._id,
+      tokenGtoP: 'tokenGtoP1',
+      tokenPtoG: 'tokenPtoG1',
+      permissionsForBroadcastUseCommands: {
+        foo: true,
+        search: false,
+      },
+      permissionsForSingleUseCommands: {
+        bar: true,
+        create: false,
+        togetter: false,
+      },
+    });
+    expect(fixedDoc2).toStrictEqual({
+      _id: doc2._id,
+      tokenGtoP: 'tokenGtoP2',
+      tokenPtoG: 'tokenPtoG2',
+      permissionsForBroadcastUseCommands: {
+        foo: true,
+        search: false,
+      },
+      permissionsForSingleUseCommands: {
+        bar: true,
+        create: false,
+        togetter: false,
+      },
+    });
+    expect(fixedDoc3).toStrictEqual({
+      _id: doc3._id,
+      tokenGtoP: 'tokenGtoP3',
+      tokenPtoG: 'tokenPtoG3',
+      permissionsForBroadcastUseCommands: {
+        search: true,
+      },
+      permissionsForSingleUseCommands: {
+        create: true,
+        togetter: true,
+      },
+    });
+  });
+
+});

+ 0 - 0
packages/app/src/test/models/config.test.js → packages/app/src/test/integration/models/config.test.js


+ 0 - 0
packages/app/src/test/models/page.test.js → packages/app/src/test/integration/models/page.test.js


+ 0 - 0
packages/app/src/test/models/share-link.test.js → packages/app/src/test/integration/models/share-link.test.js


+ 0 - 0
packages/app/src/test/models/update-post.test.js → packages/app/src/test/integration/models/update-post.test.js


+ 0 - 0
packages/app/src/test/models/user.test.js → packages/app/src/test/integration/models/user.test.js


+ 0 - 0
packages/app/src/test/service/acl.test.js → packages/app/src/test/integration/service/acl.test.js


+ 0 - 0
packages/app/src/test/service/config-manager.test.js → packages/app/src/test/integration/service/config-manager.test.js


+ 2 - 2
packages/app/src/test/service/page.test.js → packages/app/src/test/integration/service/page.test.js

@@ -505,8 +505,8 @@ describe('PageService', () => {
   describe('duplicate page', () => {
   describe('duplicate page', () => {
     let duplicateDescendantsWithStreamSpy;
     let duplicateDescendantsWithStreamSpy;
 
 
-    jest.mock('../../server/models/serializers/page-serializer');
-    const { serializePageSecurely } = require('../../server/models/serializers/page-serializer');
+    jest.mock('~/server/models/serializers/page-serializer');
+    const { serializePageSecurely } = require('~/server/models/serializers/page-serializer');
     serializePageSecurely.mockImplementation(page => page);
     serializePageSecurely.mockImplementation(page => page);
 
 
     beforeEach(async() => {
     beforeEach(async() => {

+ 0 - 0
packages/app/src/test/service/passport.test.js → packages/app/src/test/integration/service/passport.test.js


+ 0 - 0
packages/app/src/test/service/search-delegator/searchbox.test.js → packages/app/src/test/integration/service/search-delegator/searchbox.test.js


+ 1 - 5
packages/app/src/test/setup-crowi.js → packages/app/src/test/integration/setup-crowi.js

@@ -2,7 +2,7 @@ import Crowi from '~/server/crowi';
 
 
 let _instance = null;
 let _instance = null;
 
 
-async function getInstance(isNewInstance) {
+export async function getInstance(isNewInstance) {
   if (isNewInstance) {
   if (isNewInstance) {
     const crowi = new Crowi();
     const crowi = new Crowi();
     await crowi.initForTest();
     await crowi.initForTest();
@@ -16,7 +16,3 @@ async function getInstance(isNewInstance) {
   }
   }
   return _instance;
   return _instance;
 }
 }
-
-module.exports = {
-  getInstance,
-};

+ 1 - 1
packages/app/src/test/setup.js → packages/app/src/test/integration/setup.js

@@ -7,7 +7,7 @@
 
 
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
 
 
-const { initMongooseGlobalSettings, getMongoUri, mongoOptions } = require('~/server/util/mongoose-utils');
+const { initMongooseGlobalSettings, getMongoUri, mongoOptions } = require('@growi/core');
 
 
 mongoose.Promise = global.Promise;
 mongoose.Promise = global.Promise;
 
 

+ 0 - 0
packages/app/src/test/utils/slack-legacy.test.js → packages/app/src/test/integration/utils/slack-legacy.test.js


+ 0 - 0
packages/app/src/test/middlewares/safe-redirect.test.js → packages/app/src/test/unit/middlewares/safe-redirect.test.js


+ 71 - 0
packages/app/src/test/unit/migrate-mongo-config.test.js

@@ -0,0 +1,71 @@
+describe('config/migrate.js', () => {
+
+  beforeEach(async() => {
+    jest.resetModules();
+  });
+
+  test('throws an error when MIGRATIONS_DIR is not set', () => {
+
+    const initMongooseGlobalSettingsMock = jest.fn();
+
+    // mock for mongoose-utils
+    jest.doMock('@growi/core', () => {
+      return {
+        initMongooseGlobalSettings: initMongooseGlobalSettingsMock,
+      };
+    });
+
+    const requireConfig = () => {
+      require('^/migrate-mongo-config');
+    };
+
+    expect(requireConfig).toThrow('An env var MIGRATIONS_DIR must be set.');
+
+    jest.dontMock('@growi/core');
+
+    expect(initMongooseGlobalSettingsMock).not.toHaveBeenCalled();
+  });
+
+  /* eslint-disable indent */
+  describe.each`
+    MONGO_URI                                         | expectedDbName
+    ${'mongodb://example.com/growi'}                  | ${'growi'}
+    ${'mongodb://user:pass@example.com/growi'}        | ${'growi'}
+    ${'mongodb://example.com/growi?replicaSet=mySet'} | ${'growi'}
+  `('returns', ({ MONGO_URI, expectedDbName }) => {
+
+    beforeEach(async() => {
+      process.env.MIGRATIONS_DIR = 'testdir/migrations';
+    });
+
+    test(`when 'MONGO_URI' is '${MONGO_URI}`, () => {
+
+      const initMongooseGlobalSettingsMock = jest.fn();
+      const mongoOptionsMock = jest.fn();
+
+      // mock for mongoose-utils
+      jest.doMock('@growi/core', () => {
+        return {
+          initMongooseGlobalSettings: initMongooseGlobalSettingsMock,
+          getMongoUri: () => {
+            return MONGO_URI;
+          },
+          mongoOptions: mongoOptionsMock,
+        };
+      });
+
+      const { mongodb, migrationsDir, changelogCollectionName } = require('^/migrate-mongo-config');
+
+      jest.dontMock('@growi/core');
+
+      expect(initMongooseGlobalSettingsMock).toHaveBeenCalledTimes(1);
+      expect(mongodb.url).toBe(MONGO_URI);
+      expect(mongodb.databaseName).toBe(expectedDbName);
+      expect(mongodb.options).toBe(mongoOptionsMock);
+      expect(migrationsDir).toBe('testdir/migrations');
+      expect(changelogCollectionName).toBe('migrations');
+    });
+  });
+  /* eslint-enable indent */
+
+});

Неке датотеке нису приказане због велике количине промена