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

Merge branch 'master' into imprv/duplicate-Page-with-child

itizawa 5 лет назад
Родитель
Сommit
59949bc51a
43 измененных файлов с 758 добавлено и 681 удалено
  1. 9 0
      CHANGES.md
  2. 1 0
      config/webpack.prod.js
  3. 2 2
      package.json
  4. 1 1
      resource/locales/en_US/admin/admin.json
  5. 4 18
      resource/locales/en_US/translation.json
  6. 1 1
      resource/locales/ja_JP/admin/admin.json
  7. 4 18
      resource/locales/ja_JP/translation.json
  8. 2 2
      resource/locales/zh_CN/admin/admin.json
  9. 5 19
      resource/locales/zh_CN/translation.json
  10. 4 17
      src/client/js/components/Admin/App/AppSettingsPage.jsx
  11. 80 59
      src/client/js/components/Admin/App/MailSetting.jsx
  12. 4 17
      src/client/js/components/Admin/Customize/Customize.jsx
  13. 1 1
      src/client/js/components/Admin/ImportData/GrowiArchive/ErrorViewer.jsx
  14. 1 1
      src/client/js/components/Admin/ImportData/ImportDataPageContents.jsx
  15. 4 17
      src/client/js/components/Admin/ImportDataPage.jsx
  16. 4 17
      src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  17. 4 17
      src/client/js/components/Admin/Notification/NotificationSetting.jsx
  18. 4 17
      src/client/js/components/Admin/Security/BasicSecuritySetting.jsx
  19. 4 17
      src/client/js/components/Admin/Security/GitHubSecuritySetting.jsx
  20. 4 17
      src/client/js/components/Admin/Security/GoogleSecuritySetting.jsx
  21. 4 18
      src/client/js/components/Admin/Security/LdapSecuritySetting.jsx
  22. 4 17
      src/client/js/components/Admin/Security/LocalSecuritySetting.jsx
  23. 4 17
      src/client/js/components/Admin/Security/OidcSecuritySetting.jsx
  24. 4 17
      src/client/js/components/Admin/Security/SamlSecuritySetting.jsx
  25. 5 17
      src/client/js/components/Admin/Security/SecurityManagement.jsx
  26. 4 17
      src/client/js/components/Admin/Security/TwitterSecuritySetting.jsx
  27. 25 6
      src/client/js/components/Me/PasswordSettings.jsx
  28. 13 0
      src/client/js/services/AdminAppContainer.js
  29. 2 2
      src/client/js/services/PageHistoryContainer.js
  30. 0 2
      src/client/js/services/PersonalContainer.js
  31. 2 2
      src/server/middlewares/certify-shared-page.js
  32. 3 28
      src/server/models/user.js
  33. 0 17
      src/server/routes/admin.js
  34. 55 9
      src/server/routes/apiv3/app-settings.js
  35. 46 2
      src/server/routes/apiv3/personal-setting.js
  36. 2 1
      src/server/routes/apiv3/users.js
  37. 4 1
      src/server/routes/attachment.js
  38. 0 1
      src/server/routes/index.js
  39. 16 8
      src/server/service/export.js
  40. 26 18
      src/server/service/import.js
  41. 0 1
      src/server/service/search-delegator/elasticsearch.js
  42. 0 7
      src/test/models/user.test.js
  43. 396 215
      yarn.lock

+ 9 - 0
CHANGES.md

@@ -4,7 +4,16 @@
 
 * Feature: Create/edit linker with GUI
 * Improvement: Paging page histories
+* Improvement: Avoid using `cursor.snapshot()` in preparation for MongoDB version upgrade
+* Improvement: Allow to save "From e-mail address" only in App Settings
+* Improvement: Allow to empty "From e-mail address" in App Settings
+* Improvement: Export/Import archive data serially so as not to waste memory
 * Fix: To be able to delete attachment metadata even when the actual data does not exist
+* Fix: Limit the attrubutes of user data for `/_api/v3/users`
+* Fix: Prevent XSS with SVG
+* Upgrade libs
+    * optimize-css-assets-webpack-plugin
+    * terser-webpack-plugin
 
 ## v4.1.2
 

+ 1 - 0
config/webpack.prod.js

@@ -67,6 +67,7 @@ module.exports = require('./webpack.common')({
 
   ],
   optimization: {
+    minimize: true,
     minimizer: [
       new TerserPlugin({}),
       new OptimizeCSSAssetsPlugin({}),

+ 2 - 2
package.json

@@ -220,7 +220,7 @@
     "normalize-path": "^3.0.0",
     "null-loader": "^3.0.0",
     "on-headers": "^1.0.1",
-    "optimize-css-assets-webpack-plugin": "^5.0.0",
+    "optimize-css-assets-webpack-plugin": "^5.0.3",
     "penpal": "^4.0.0",
     "plantuml-encoder": "^1.2.5",
     "postcss-loader": "^3.0.0",
@@ -249,7 +249,7 @@
     "stylelint-config-recess-order": "^2.0.1",
     "swagger-jsdoc": "^3.4.0",
     "swagger2openapi": "^5.3.1",
-    "terser-webpack-plugin": "^2.0.1",
+    "terser-webpack-plugin": "^4.1.0",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",
     "unstated": "^2.1.1",

+ 1 - 1
resource/locales/en_US/admin/admin.json

@@ -210,7 +210,7 @@
     "export_collections": "Export Collections",
     "check_all": "Check All",
     "uncheck_all": "Uncheck All",
-    "desc_password_seed": "DO NOT FORGET to set current <code>PASSWORD_SEED</code> to your new GROWI system when restoring user data, or users will NOT be able to login with their password.<br><br><strong>HINT:</strong><br>The current <code>PASSWORD_SEED</code> will be stored in <code>meta.json</code> in exported ZIP.",
+    "desc_password_seed": "<p>DO NOT FORGET to set current <code>PASSWORD_SEED</code> to your new GROWI system when restoring user data, or users will NOT be able to login with their password.<br><br><strong>HINT:</strong><br>The current <code>PASSWORD_SEED</code> will be stored in <code>meta.json</code> in exported ZIP.</p>",
     "create_new_archive_data": "Create New Archive Data",
     "export": "Export",
     "cancel": "Cancel",

+ 4 - 18
resource/locales/en_US/translation.json

@@ -715,24 +715,6 @@
     "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
     "rebuild_description_2": "This may take a while."
   },
-  "export_management": {
-    "exporting_collection_list": "Exporting Collection List",
-    "exported_data_list": "Exported Archive Data List",
-    "export_collections": "Export Collections",
-    "check_all": "Check All",
-    "uncheck_all": "Uncheck All",
-    "desc_password_seed": "<p>DO NOT FORGET to set current <code>PASSWORD_SEED</code> to your new GROWI system when restoring user data, or users will NOT be able to login with their password.</p><strong>HINT:</strong><p>The current <code>PASSWORD_SEED</code> will be stored in <code>meta.json</code> in exported ZIP.</p>",
-    "create_new_archive_data": "Create New Archive Data",
-    "export": "Export",
-    "cancel": "Cancel",
-    "file": "File",
-    "growi_version": "Growi Version",
-    "collections": "Collections",
-    "exported_at": "Exported At",
-    "export_menu": "Export Menu",
-    "download": "Download",
-    "delete": "Delete"
-  },
   "login": {
     "Sign in error": "Login error",
     "Registration successful": "Registration successful",
@@ -766,5 +748,9 @@
     "complete_to_install1":"Complete to Install GROWI ! Please login as admin account.",
     "complete_to_install2":"Complete to Install GROWI ! Please check each settings on this page first.",
     "failed_to_create_admin_user":"Failed to create admin user. {{errMessage}}"
+  },
+  "validation":{
+    "aws_region": "For the region, enter the AWS region name. ex):us-east-1",
+    "aws_custom_endpoint":"For the custom endpoint, specify the URL that starts with http(s)://. Also, the trailing slash is not required."
   }
 }

+ 1 - 1
resource/locales/ja_JP/admin/admin.json

@@ -147,7 +147,7 @@
     "export_collections": "コレクションのエクスポート",
     "check_all": "全てにチェックを付ける",
     "uncheck_all": "全てからチェックを外す",
-    "desc_password_seed": "ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。<br><br><strong>ヒント:</strong><br>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。",
+    "desc_password_seed": "<p>ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。<br><br><strong>ヒント:</strong><br>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。</p>",
     "create_new_archive_data": "アーカイブデータの新規作成",
     "export": "エクスポート",
     "cancel": "キャンセル",

+ 4 - 18
resource/locales/ja_JP/translation.json

@@ -708,24 +708,6 @@
     "rebuild_description_1": "全てのページのインデックスを削除し、作り直します。",
     "rebuild_description_2": "この作業には数秒かかります。"
   },
-  "export_management": {
-    "exporting_collection_list": "エクスポート中のコレクション",
-    "exported_data_list": "エクスポートされたアーカイブリスト",
-    "export_collections": "コレクションのエクスポート",
-    "check_all": "全てにチェックを付ける",
-    "uncheck_all": "全てからチェックを外す",
-    "desc_password_seed": "<p>ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。</p><strong>ヒント:</strong><p>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。</p>",
-    "create_new_archive_data": "アーカイブデータの新規作成",
-    "export": "エクスポート",
-    "cancel": "キャンセル",
-    "file": "ファイル名",
-    "growi_version": "Growi バージョン",
-    "collections": "コレクション",
-    "exported_at": "エクスポートされた時間",
-    "export_menu": "エクスポートメニュー",
-    "download": "ダウンロード",
-    "delete": "削除"
-  },
   "login": {
     "Sign in error": "ログインエラー",
     "Registration successful": "登録完了",
@@ -759,5 +741,9 @@
     "complete_to_install1":"GROWI のインストールが完了しました!管理者アカウントでログインしてください。",
     "complete_to_install2":"GROWI のインストールが完了しました!はじめに、このページで各種設定を確認してください。",
     "failed_to_create_admin_user":"管理ユーザーの作成に失敗しました。{{errMessage}}"
+  },
+  "validation":{
+    "aws_region": "リージョンには、AWSリージョン名を入力してください。例: ap-northeast-1",
+    "aws_custom_endpoint": "カスタムエンドポイントは、http(s)://で始まるURLを指定してください。また、末尾の/は不要です。"
   }
 }

+ 2 - 2
resource/locales/zh_CN/admin/admin.json

@@ -225,7 +225,7 @@
 		"export_collections": "导出集合",
 		"check_all": "全部检查",
 		"uncheck_all": "全部取消选中",
-		"desc_password_seed": "DO NOT FORGET to set current <code>PASSWORD_SEED</code> to your new GROWI system when restoring user data, or users will NOT be able to login with their password.<br><br><strong>HINT:</strong><br>The current <code>PASSWORD_SEED</code> will be stored in <code>meta.json</code> in exported ZIP.",
+		"desc_password_seed": "<p>还原用户数据时,不要忘记将当前的<code>密码种子设置到新的GROWI系统,否则用户将无法使用其密码登录。<br><br><strong>提示:</strong><br>当前的<code>密码种子将存储在<code>meta.json格式</code>在导出的zip压缩包中。</p>",
 		"create_new_archive_data": "创建新的存档数据",
 		"export": "导出",
 		"cancel": "取消",
@@ -236,7 +236,7 @@
 		"export_menu": "导出菜单",
 		"download": "下载",
 		"delete": "删除"
-	},
+  },
 	"user_management": {
 		"invite_users": "邀请新用户",
 		"click_twice_same_checkbox": "您应该至少选中一个复选框。",

+ 5 - 19
resource/locales/zh_CN/translation.json

@@ -697,24 +697,6 @@
 		"rebuild_description_1": "单击按钮以重新生成索引并添加所有页面数据。",
 		"rebuild_description_2": "这可能需要一段时间。"
 	},
-	"export_management": {
-		"exporting_collection_list": "正在导出集合列表",
-		"exported_data_list": "导出的存档数据列表",
-		"export_collections": "导出集合",
-		"check_all": "全部选中",
-		"uncheck_all": "全部取消选中",
-		"desc_password_seed": "还原用户数据时,不要忘记将当前的<code>密码种子设置到新的GROWI系统,否则用户将无法使用其密码登录。<br><br><strong>提示:</strong><br>当前的<code>密码种子将存储在<code>meta.json格式</code>在导出的zip压缩包中。",
-		"create_new_archive_data": "创建新的存档数据",
-		"export": "导出",
-		"cancel": "取消",
-		"file": "文件",
-		"growi_version": "Growi 版本号",
-		"collections": "收藏",
-		"exported_at": "导出在",
-		"export_menu": "导出菜单",
-		"download": "下载",
-		"delete": "删除"
-	},
 	"personal_dropdown": {
 		"home": "家",
 		"settings": "设置",
@@ -760,5 +742,9 @@
 		"complete_to_install1": "完成安装GROWI!请以管理员帐户登录。",
 		"complete_to_install2": "完成安装GROWI!请先检查此页上的每个设置。",
 		"failed_to_create_admin_user": "无法创建管理用户。{{errMessage}"
-	}
+  },
+  "validation":{
+    "aws_region": "关于地区,请输入AWS地区名,例如:ap-east-1",
+    "aws_custom_endpoint": "关于自定义端点,请指定以http(s)://开头的URL,链接末尾不需要添加“/”"
+  }
 }

+ 4 - 17
src/client/js/components/Admin/App/AppSettingsPage.jsx

@@ -1,10 +1,11 @@
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import loggerFactory from '@alias/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminAppContainer from '../../../services/AdminAppContainer';
 
@@ -12,20 +13,6 @@ import AppSettingsPageContents from './AppSettingsPageContents';
 
 const logger = loggerFactory('growi:appSettings');
 
-function AppSettingsPageWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <AppSettingsPageWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
 let retrieveErrors = null;
 function AppSettingsPage(props) {
   if (props.adminAppContainer.state.title === props.adminAppContainer.dummyTitle) {
@@ -59,6 +46,6 @@ AppSettingsPage.propTypes = {
 /**
  * Wrapper component for using unstated
  */
-const AppSettingsPageWithUnstatedContainer = withUnstatedContainers(AppSettingsPage, [AdminAppContainer]);
+const AppSettingsPageWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(AppSettingsPage), [AdminAppContainer]);
 
-export default AppSettingsPageWithContainerWithSuspense;
+export default AppSettingsPageWithUnstatedContainer;

+ 80 - 59
src/client/js/components/Admin/App/MailSetting.jsx

@@ -29,6 +29,7 @@ class MailSetting extends React.Component {
 
     this.openInitializeValueModal = this.openInitializeValueModal.bind(this);
     this.closeInitializeValueModal = this.closeInitializeValueModal.bind(this);
+    this.submitFromAdressHandler = this.submitFromAdressHandler.bind(this);
     this.submitHandler = this.submitHandler.bind(this);
     this.initialize = this.initialize.bind(this);
   }
@@ -54,14 +55,26 @@ class MailSetting extends React.Component {
     }
   }
 
+  async submitFromAdressHandler() {
+    const { t, adminAppContainer } = this.props;
+
+    try {
+      await adminAppContainer.updateFromAdressHandler();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.mail_settings') }));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }
+
   async initialize() {
     const { t, adminAppContainer } = this.props;
 
     try {
       const mailSettingParams = await adminAppContainer.initializeMailSettingHandler();
-      toastSuccess(t('toaster.initialize_successed', { target: t('admin:app_setting.mail_settings') }));
+      toastSuccess(t('toaster.initialize_successed', { target: t('admin:app_setting.smtp_settings') }));
       // convert values to '' if value is null for overwriting values of inputs with refs
-      this.emailInput.current.value = mailSettingParams.fromAddress || '';
       this.hostInput.current.value = mailSettingParams.smtpHost || '';
       this.portInput.current.value = mailSettingParams.smtpPort || '';
       this.userInput.current.value = mailSettingParams.smtpUser || '';
@@ -93,70 +106,78 @@ class MailSetting extends React.Component {
             />
           </div>
         </div>
-
-        <div className="row form-group mb-5">
-          <label className="col-md-3 col-form-label text-left">{t('admin:app_setting.smtp_settings')}</label>
-          <div className="col-md-4">
-            <label>{t('admin:app_setting.host')}</label>
-            <input
-              className="form-control"
-              type="text"
-              ref={this.hostInput}
-              defaultValue={adminAppContainer.state.smtpHost || ''}
-              onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
-            />
-          </div>
-          <div className="col-md-2">
-            <label>{t('admin:app_setting.port')}</label>
-            <input
-              className="form-control"
-              ref={this.portInput}
-              defaultValue={adminAppContainer.state.smtpPort || ''}
-              onChange={(e) => { adminAppContainer.changeSmtpPort(e.target.value) }}
-            />
+        <div className="row my-3">
+          <div className="mx-auto">
+            <button type="button" className="btn btn-primary" onClick={this.submitFromAdressHandler}>{ t('Update') }</button>
           </div>
         </div>
-
-        <div className="row form-group mb-5">
-          <div className="col-md-3 offset-md-3">
-            <label>{t('admin:app_setting.user')}</label>
-            <input
-              className="form-control"
-              type="text"
-              ref={this.userInput}
-              defaultValue={adminAppContainer.state.smtpUser || ''}
-              onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
-            />
-          </div>
-          <div className="col-md-3">
-            <label>{t('Password')}</label>
-            <input
-              className="form-control"
-              type="password"
-              ref={this.passwordInput}
-              defaultValue={adminAppContainer.state.smtpPassword || ''}
-              onChange={(e) => { adminAppContainer.changeSmtpPassword(e.target.value) }}
-            />
+        <div id="mail-smtp" className="tab-pane active mt-5">
+          <div className="row form-group mb-5">
+            <label className="col-md-3 col-form-label text-left">{t('admin:app_setting.smtp_settings')}</label>
+            <div className="col-md-4">
+              <label>{t('admin:app_setting.host')}</label>
+              <input
+                className="form-control"
+                type="text"
+                ref={this.hostInput}
+                defaultValue={adminAppContainer.state.smtpHost || ''}
+                onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
+              />
+            </div>
+            <div className="col-md-2">
+              <label>{t('admin:app_setting.port')}</label>
+              <input
+                className="form-control"
+                ref={this.portInput}
+                defaultValue={adminAppContainer.state.smtpPort || ''}
+                onChange={(e) => { adminAppContainer.changeSmtpPort(e.target.value) }}
+              />
+            </div>
           </div>
-        </div>
 
-        <div className="row my-3">
-          <div className="offset-5">
-            <button type="button" className="btn btn-primary" onClick={this.submitHandler} disabled={adminAppContainer.state.retrieveError != null}>
-              { t('Update') }
-            </button>
+          <div className="row form-group mb-5">
+            <div className="col-md-3 offset-md-3">
+              <label>{t('admin:app_setting.user')}</label>
+              <input
+                className="form-control"
+                type="text"
+                ref={this.userInput}
+                defaultValue={adminAppContainer.state.smtpUser || ''}
+                onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
+              />
+            </div>
+            <div className="col-md-3">
+              <label>{t('Password')}</label>
+              <input
+                className="form-control"
+                type="password"
+                ref={this.passwordInput}
+                defaultValue={adminAppContainer.state.smtpPassword || ''}
+                onChange={(e) => { adminAppContainer.changeSmtpPassword(e.target.value) }}
+              />
+            </div>
           </div>
-          <div className="offset-1">
-            <button
-              type="button"
-              className="btn btn-secondary"
-              onClick={this.openInitializeValueModal}
-              disabled={adminAppContainer.state.retrieveError != null}
-            >
-              {t('admin:app_setting.initialize_mail_settings')}
-            </button>
+
+          <div className="row my-3">
+            <div className="offset-5">
+              <button type="button" className="btn btn-primary" onClick={this.submitHandler} disabled={adminAppContainer.state.retrieveError != null}>
+                { t('Update') }
+              </button>
+            </div>
+            <div className="offset-1">
+              <button
+                type="button"
+                className="btn btn-secondary"
+                onClick={this.openInitializeValueModal}
+                disabled={adminAppContainer.state.retrieveError != null}
+              >
+                {t('admin:app_setting.initialize_mail_settings')}
+              </button>
+            </div>
           </div>
         </div>
+
+
         <Modal isOpen={this.state.isInitializeValueModalOpen} toggle={this.closeInitializeValueModal} className="initialize-mail-settings">
           <ModalHeader tag="h4" toggle={this.closeInitializeValueModal} className="bg-danger text-light">
             {t('admin:app_setting.initialize_mail_modal_header')}

+ 4 - 17
src/client/js/components/Admin/Customize/Customize.jsx

@@ -1,5 +1,5 @@
 
-import React, { Fragment, Suspense } from 'react';
+import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 
 import loggerFactory from '@alias/logger';
@@ -8,6 +8,7 @@ import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
@@ -19,20 +20,6 @@ import CustomizeTitle from './CustomizeTitle';
 
 const logger = loggerFactory('growi:services:AdminCustomizePage');
 
-function CustomizePageWithContainerWithSusupense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <CustomizePageWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
 let retrieveErrors = null;
 function Customize(props) {
   const { adminCustomizeContainer } = props;
@@ -83,10 +70,10 @@ function Customize(props) {
   );
 }
 
-const CustomizePageWithUnstatedContainer = withUnstatedContainers(Customize, [AdminCustomizeContainer]);
+const CustomizePageWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(Customize), [AdminCustomizeContainer]);
 
 Customize.propTypes = {
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
 };
 
-export default CustomizePageWithContainerWithSusupense;
+export default CustomizePageWithUnstatedContainer;

+ 1 - 1
src/client/js/components/Admin/ImportData/GrowiArchive/ErrorViewer.jsx

@@ -20,7 +20,7 @@ class ErrorViewer extends React.Component {
     }
 
     return (
-      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
+      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} size="lg">
         <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-danger text-light">
           Errors
         </ModalHeader>

+ 1 - 1
src/client/js/components/Admin/ImportData/ImportDataPageContents.jsx

@@ -189,7 +189,7 @@ class ImportDataPageContents extends React.Component {
                   className="form-control"
                   type="password"
                   name="qiitaAccessToken"
-                  value={adminImportContainer.stateqiitaAccessToken}
+                  value={adminImportContainer.state.qiitaAccessToken}
                   onChange={adminImportContainer.handleInputValue}
                 />
               </div>

+ 4 - 17
src/client/js/components/Admin/ImportDataPage.jsx

@@ -1,9 +1,10 @@
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import loggerFactory from '@alias/logger';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import toArrayIfNot from '../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../SuspenseUtils';
 
 import AdminImportContainer from '../../services/AdminImportContainer';
 import { toastError } from '../../util/apiNotification';
@@ -12,20 +13,6 @@ import ImportDataPageContents from './ImportData/ImportDataPageContents';
 
 const logger = loggerFactory('growi:importer');
 
-function ImportDataPageWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <ImportDataPageWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
 let retrieveErrors = null;
 function ImportDataPage(props) {
   const { adminImportContainer } = props;
@@ -60,6 +47,6 @@ ImportDataPage.propTypes = {
 /**
  * Wrapper component for using unstated
  */
-const ImportDataPageWithUnstatedContainer = withUnstatedContainers(ImportDataPage, [AdminImportContainer]);
+const ImportDataPageWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(ImportDataPage), [AdminImportContainer]);
 
-export default ImportDataPageWithContainerWithSuspense;
+export default ImportDataPageWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/MarkdownSetting/MarkDownSetting.jsx

@@ -1,4 +1,4 @@
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import loggerFactory from '@alias/logger';
@@ -6,26 +6,13 @@ import loggerFactory from '@alias/logger';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import MarkDownSettingContents from './MarkDownSettingContents';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 const logger = loggerFactory('growi:MarkDown');
 
-function MarkdownSettingWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <MarkdownSettingWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
 let retrieveErrors = null;
 function MarkdownSetting(props) {
   const { adminMarkDownContainer } = props;
@@ -52,10 +39,10 @@ function MarkdownSetting(props) {
   return <MarkDownSettingContents />;
 }
 
-const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(MarkdownSetting, [AdminMarkDownContainer]);
+const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(MarkdownSetting), [AdminMarkDownContainer]);
 
 MarkdownSetting.propTypes = {
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 };
 
-export default MarkdownSettingWithContainerWithSuspense;
+export default MarkdownSettingWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Notification/NotificationSetting.jsx

@@ -1,4 +1,4 @@
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import loggerFactory from '@alias/logger';
@@ -6,6 +6,7 @@ import loggerFactory from '@alias/logger';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminNotificationContainer from '../../../services/AdminNotificationContainer';
 
@@ -13,20 +14,6 @@ import NotificationSettingContents from './NotificationSettingContents';
 
 const logger = loggerFactory('growi:NotificationSetting');
 
-function NotificationSettingWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <NotificationSettingWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
 let retrieveErrors = null;
 function NotificationSetting(props) {
   const { adminNotificationContainer } = props;
@@ -52,10 +39,10 @@ function NotificationSetting(props) {
   return <NotificationSettingContents />;
 }
 
-const NotificationSettingWithUnstatedContainer = withUnstatedContainers(NotificationSetting, [AdminNotificationContainer]);
+const NotificationSettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(NotificationSetting), [AdminNotificationContainer]);
 
 NotificationSetting.propTypes = {
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
 };
 
-export default NotificationSettingWithContainerWithSuspense;
+export default NotificationSettingWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Security/BasicSecuritySetting.jsx

@@ -1,10 +1,11 @@
 /* eslint-disable react/no-danger */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminBasicSecurityContainer from '../../../services/AdminBasicSecurityContainer';
 
@@ -43,22 +44,8 @@ BasicSecurityManagement.propTypes = {
   adminBasicSecurityContainer: PropTypes.instanceOf(AdminBasicSecurityContainer).isRequired,
 };
 
-const BasicSecurityManagementWithUnstatedContainer = withUnstatedContainers(BasicSecurityManagement, [
+const BasicSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(BasicSecurityManagement), [
   AdminBasicSecurityContainer,
 ]);
 
-function BasicSecurityManagementWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <BasicSecurityManagementWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-export default BasicSecurityManagementWithContainerWithSuspense;
+export default BasicSecurityManagementWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Security/GitHubSecuritySetting.jsx

@@ -1,10 +1,11 @@
 /* eslint-disable react/no-danger */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminGitHubSecurityContainer from '../../../services/AdminGitHubSecurityContainer';
 
@@ -39,22 +40,8 @@ GitHubSecurityManagement.propTypes = {
   adminGitHubSecurityContainer: PropTypes.instanceOf(AdminGitHubSecurityContainer).isRequired,
 };
 
-const GitHubSecurityManagementWithUnstatedContainer = withUnstatedContainers(GitHubSecurityManagement, [
+const GitHubSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(GitHubSecurityManagement), [
   AdminGitHubSecurityContainer,
 ]);
 
-function GitHubSecurityManagementWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <GitHubSecurityManagementWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-export default GitHubSecurityManagementWithContainerWithSuspense;
+export default GitHubSecurityManagementWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Security/GoogleSecuritySetting.jsx

@@ -1,10 +1,11 @@
 /* eslint-disable react/no-danger */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminGoogleSecurityContainer from '../../../services/AdminGoogleSecurityContainer';
 import GoogleSecurityManagementContents from './GoogleSecuritySettingContents';
@@ -38,22 +39,8 @@ GoogleSecurityManagement.propTypes = {
   adminGoogleSecurityContainer: PropTypes.instanceOf(AdminGoogleSecurityContainer).isRequired,
 };
 
-const GoogleSecurityManagementWithUnstatedContainer = withUnstatedContainers(GoogleSecurityManagement, [
+const GoogleSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(GoogleSecurityManagement), [
   AdminGoogleSecurityContainer,
 ]);
 
-function GoogleSecurityManagementWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <GoogleSecurityManagementWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-export default GoogleSecurityManagementWithContainerWithSuspense;
+export default GoogleSecurityManagementWithUnstatedContainer;

+ 4 - 18
src/client/js/components/Admin/Security/LdapSecuritySetting.jsx

@@ -1,9 +1,10 @@
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminLdapSecurityContainer from '../../../services/AdminLdapSecurityContainer';
 
@@ -37,23 +38,8 @@ LdapSecuritySetting.propTypes = {
   adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,
 };
 
-const LdapSecuritySettingWithUnstatedContainer = withUnstatedContainers(LdapSecuritySetting, [
+const LdapSecuritySettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(LdapSecuritySetting), [
   AdminLdapSecurityContainer,
 ]);
 
-function LdapSecuritySettingWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <LdapSecuritySettingWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-
-export default LdapSecuritySettingWithContainerWithSuspense;
+export default LdapSecuritySettingWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Security/LocalSecuritySetting.jsx

@@ -1,10 +1,11 @@
 /* eslint-disable react/no-danger */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminLocalSecurityContainer from '../../../services/AdminLocalSecurityContainer';
 
@@ -38,22 +39,8 @@ LocalSecuritySetting.propTypes = {
   adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer).isRequired,
 };
 
-const LocalSecuritySettingWithUnstatedContainer = withUnstatedContainers(LocalSecuritySetting, [
+const LocalSecuritySettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(LocalSecuritySetting), [
   AdminLocalSecurityContainer,
 ]);
 
-function LocalSecuritySettingWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-)}
-    >
-      <LocalSecuritySettingWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-export default LocalSecuritySettingWithContainerWithSuspense;
+export default LocalSecuritySettingWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Security/OidcSecuritySetting.jsx

@@ -1,10 +1,11 @@
 /* eslint-disable react/no-danger */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminOidcSecurityContainer from '../../../services/AdminOidcSecurityContainer';
 
@@ -38,22 +39,8 @@ OidcSecurityManagement.propTypes = {
   adminOidcSecurityContainer: PropTypes.instanceOf(AdminOidcSecurityContainer).isRequired,
 };
 
-const OidcSecurityManagementWithUnstatedContainer = withUnstatedContainers(OidcSecurityManagement, [
+const OidcSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(OidcSecurityManagement), [
   AdminOidcSecurityContainer,
 ]);
 
-function OidcSecurityManagementWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <OidcSecurityManagementWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-export default OidcSecurityManagementWithContainerWithSuspense;
+export default OidcSecurityManagementWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Security/SamlSecuritySetting.jsx

@@ -1,5 +1,5 @@
 /* eslint-disable react/no-danger */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -7,6 +7,7 @@ import { toastError } from '../../../util/apiNotification';
 
 import AdminSamlSecurityContainer from '../../../services/AdminSamlSecurityContainer';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import SamlSecuritySettingContents from './SamlSecuritySettingContents';
 
@@ -38,22 +39,8 @@ SamlSecurityManagement.propTypes = {
   adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer).isRequired,
 };
 
-const SamlSecurityManagementWithUnstatedContainer = withUnstatedContainers(SamlSecurityManagement, [
+const SamlSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(SamlSecurityManagement), [
   AdminSamlSecurityContainer,
 ]);
 
-function SamlSecurityManagementWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <SamlSecurityManagementWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-export default SamlSecurityManagementWithContainerWithSuspense;
+export default SamlSecurityManagementWithUnstatedContainer;

+ 5 - 17
src/client/js/components/Admin/Security/SecurityManagement.jsx

@@ -1,9 +1,11 @@
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import { toastError } from '../../../util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
+
 import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
 import SecurityManagementContents from './SecurityManagementContents';
 
@@ -38,20 +40,6 @@ SecurityManagement.propTypes = {
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
 };
 
-const SecurityManagementWithUnstatedContainer = withUnstatedContainers(SecurityManagement, [AdminGeneralSecurityContainer]);
-
-function SecurityManagementWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <SecurityManagementWithUnstatedContainer {...props} />
-    </Suspense>
-  );
-}
+const SecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(SecurityManagement), [AdminGeneralSecurityContainer]);
 
-export default SecurityManagementWithContainerWithSuspense;
+export default SecurityManagementWithUnstatedContainer;

+ 4 - 17
src/client/js/components/Admin/Security/TwitterSecuritySetting.jsx

@@ -1,10 +1,11 @@
 /* eslint-disable react/no-danger */
-import React, { Suspense } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
+import { withLoadingSppiner } from '../../SuspenseUtils';
 
 import AdminTwitterSecurityContainer from '../../../services/AdminTwitterSecurityContainer';
 
@@ -40,22 +41,8 @@ TwitterSecurityManagement.propTypes = {
   adminTwitterSecurityContainer: PropTypes.instanceOf(AdminTwitterSecurityContainer).isRequired,
 };
 
-const TwitterSecurityManagementWithUnstatedContainer = withUnstatedContainers(TwitterSecurityManagement, [
+const TwitterSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(TwitterSecurityManagement), [
   AdminTwitterSecurityContainer,
 ]);
 
-function TwitterSecurityManagementWithContainerWithSuspense(props) {
-  return (
-    <Suspense
-      fallback={(
-        <div className="row">
-          <i className="fa fa-5x fa-spinner fa-pulse mx-auto text-muted"></i>
-        </div>
-      )}
-    >
-      <TwitterSecurityManagementWithUnstatedContainer />
-    </Suspense>
-  );
-}
-
-export default TwitterSecurityManagementWithContainerWithSuspense;
+export default TwitterSecurityManagementWithUnstatedContainer;

+ 25 - 6
src/client/js/components/Me/PasswordSettings.jsx

@@ -3,7 +3,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-
 import { toastSuccess, toastError } from '../../util/apiNotification';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
@@ -23,6 +22,7 @@ class PasswordSettings extends React.Component {
       oldPassword: '',
       newPassword: '',
       newPasswordConfirm: '',
+      isPasswordSet: false,
     };
 
     this.onClickSubmit = this.onClickSubmit.bind(this);
@@ -30,6 +30,21 @@ class PasswordSettings extends React.Component {
 
   }
 
+  async componentDidMount() {
+    const { appContainer } = this.props;
+
+    try {
+      const res = await appContainer.apiv3Get('/personal-setting/is-password-set');
+      const { isPasswordSet } = res.data;
+      this.setState({ isPasswordSet });
+    }
+    catch (err) {
+      toastError(err);
+      this.setState({ retrieveError: err });
+    }
+
+  }
+
   async onClickSubmit() {
     const { t, appContainer, personalContainer } = this.props;
     const { oldPassword, newPassword, newPasswordConfirm } = this.state;
@@ -61,22 +76,26 @@ class PasswordSettings extends React.Component {
   }
 
   render() {
-    const { t, personalContainer } = this.props;
+    const { t } = this.props;
     const { newPassword, newPasswordConfirm } = this.state;
     const isIncorrectConfirmPassword = (newPassword !== newPasswordConfirm);
 
+    if (this.state.retrieveError != null) {
+      throw new Error(this.state.retrieveError.message);
+    }
+
     return (
       <React.Fragment>
-        { (!personalContainer.state.isPasswordSet) && (
+        { (!this.state.isPasswordSet) && (
           <div className="alert alert-warning">{ t('personal_settings.password_is_not_set') }</div>
         ) }
 
         <div className="container-fluid my-4">
-          {(personalContainer.state.isPasswordSet)
+          {(this.state.isPasswordSet)
             ? <h2 className="border-bottom">{t('personal_settings.update_password')}</h2>
           : <h2 className="border-bottom">{t('personal_settings.set_new_password')}</h2>}
         </div>
-        {(personalContainer.state.isPasswordSet)
+        {(this.state.isPasswordSet)
         && (
           <div className="row mb-3">
             <label htmlFor="oldPassword" className="col-md-3 text-md-right">{ t('personal_settings.current_password') }</label>
@@ -124,7 +143,7 @@ class PasswordSettings extends React.Component {
               type="button"
               className="btn btn-primary"
               onClick={this.onClickSubmit}
-              disabled={this.state.retrieveError != null || isIncorrectConfirmPassword}
+              disabled={isIncorrectConfirmPassword}
             >
               {t('Update')}
             </button>

+ 13 - 0
src/client/js/services/AdminAppContainer.js

@@ -238,6 +238,19 @@ export default class AdminAppContainer extends Container {
     return siteUrlSettingParams;
   }
 
+  /**
+   * Update from adress
+   * @memberOf AdminAppContainer
+   * @return {Array} Appearance
+   */
+  async updateFromAdressHandler() {
+    const response = await this.appContainer.apiv3.put('/app-settings/from-address', {
+      fromAddress: this.state.fromAddress,
+    });
+    const { mailSettingParams } = response.data;
+    return mailSettingParams;
+  }
+
   /**
    * Update mail setting
    * @memberOf AdminAppContainer

+ 2 - 2
src/client/js/services/PageHistoryContainer.js

@@ -55,7 +55,7 @@ export default class PageHistoryContainer extends Container {
       return;
     }
 
-    const res = await this.appContainer.apiv3Get('/revisions/list', { pageId, share_link_id: shareLinkId, selectedPage });
+    const res = await this.appContainer.apiv3Get('/revisions/list', { pageId, shareLinkId, selectedPage });
     const rev = res.data.docs;
 
     // set Pagination state
@@ -138,7 +138,7 @@ export default class PageHistoryContainer extends Container {
     }
 
     try {
-      const res = await this.appContainer.apiv3Get(`/revisions/${revision._id}`, { pageId, share_link_id: shareLinkId });
+      const res = await this.appContainer.apiv3Get(`/revisions/${revision._id}`, { pageId, shareLinkId });
       this.setState({
         revisions: this.state.revisions.map((rev) => {
           // comparing ObjectId

+ 0 - 2
src/client/js/services/PersonalContainer.js

@@ -29,7 +29,6 @@ export default class PersonalContainer extends Container {
       isUploadedPicture: false,
       uploadedPictureSrc: this.getUploadedPictureSrc(this.appContainer.currentUser),
       externalAccounts: [],
-      isPasswordSet: false,
       apiToken: '',
     };
 
@@ -55,7 +54,6 @@ export default class PersonalContainer extends Container {
         isEmailPublished: currentUser.isEmailPublished,
         lang: currentUser.lang,
         isGravatarEnabled: currentUser.isGravatarEnabled,
-        isPasswordSet: (currentUser.password != null),
         apiToken: currentUser.apiToken,
       });
     }

+ 2 - 2
src/server/middlewares/certify-shared-page.js

@@ -5,8 +5,8 @@ const logger = loggerFactory('growi:middleware:certify-shared-page');
 module.exports = (crowi) => {
 
   return async(req, res, next) => {
-    const pageId = req.query.page_id || req.body.page_id || null;
-    const shareLinkId = req.query.share_link_id || req.body.share_link_id || null;
+    const pageId = req.query.pageId || req.body.pageId || null;
+    const shareLinkId = req.query.shareLinkId || req.body.shareLinkId || null;
     if (pageId == null || shareLinkId == null) {
       return next();
     }

+ 3 - 28
src/server/models/user.js

@@ -20,7 +20,7 @@ module.exports = function(crowi) {
   const STATUS_DELETED = 4;
   const STATUS_INVITED = 5;
   const USER_PUBLIC_FIELDS = '_id image isEmailPublished isGravatarEnabled googleId name username email introduction'
-  + 'status lang createdAt lastLoginAt admin imageUrlCached';
+  + ' status lang createdAt lastLoginAt admin imageUrlCached';
 
   const PAGE_ITEMS = 50;
 
@@ -384,7 +384,7 @@ module.exports = function(crowi) {
     option = option || {};
 
     const sort = option.sort || { createdAt: -1 };
-    const fields = option.fields || USER_PUBLIC_FIELDS;
+    const fields = option.fields || {};
 
     let status = option.status || [STATUS_ACTIVE, STATUS_SUSPENDED];
     if (!Array.isArray(status)) {
@@ -403,7 +403,7 @@ module.exports = function(crowi) {
 
     const sort = option.sort || { createdAt: -1 };
     const status = option.status || STATUS_ACTIVE;
-    const fields = option.fields || USER_PUBLIC_FIELDS;
+    const fields = option.fields || {};
 
     return this.find({ _id: { $in: ids }, status })
       .select(fields)
@@ -414,31 +414,6 @@ module.exports = function(crowi) {
     return this.find({ admin: true });
   };
 
-  userSchema.statics.findUsersByPartOfEmail = function(emailPart, options) {
-    const status = options.status || null;
-    const emailPartRegExp = new RegExp(emailPart.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'));
-    const User = this;
-
-    return new Promise((resolve, reject) => {
-      const query = User.find({ email: emailPartRegExp }, USER_PUBLIC_FIELDS);
-
-      if (status) {
-        query.and({ status });
-      }
-
-      query
-        .limit(PAGE_ITEMS + 1)
-        .exec((err, userData) => {
-          if (err) {
-            return reject(err);
-          }
-
-          return resolve(userData);
-        });
-    });
-  };
-
-
   userSchema.statics.findUserByUsername = function(username) {
     if (username == null) {
       return Promise.resolve(null);

+ 0 - 17
src/server/routes/admin.js

@@ -337,23 +337,6 @@ module.exports = function(crowi, app) {
 
   actions.api = {};
 
-  // app.get('/_api/admin/users.search' , admin.api.userSearch);
-  actions.api.usersSearch = function(req, res) {
-    const User = crowi.model('User');
-    const email = req.query.email;
-
-    User.findUsersByPartOfEmail(email, {})
-      .then((users) => {
-        const result = {
-          data: users,
-        };
-        return res.json(ApiResponse.success(result));
-      })
-      .catch((err) => {
-        return res.json(ApiResponse.error());
-      });
-  };
-
   /**
    * save esa settings, update config cache, and response json
    *

+ 55 - 9
src/server/routes/apiv3/app-settings.js

@@ -50,13 +50,17 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *          envSiteUrl:
  *            type: string
  *            description: environment variable 'APP_SITE_URL'
- *      MailSettingParams:
+ *      FromAddress:
  *        description: MailSettingParams
  *        type: object
  *        properties:
  *          fromAddress:
  *            type: string
  *            description: e-mail address used as from address of mail which sent from GROWI app
+ *      MailSettingParams:
+ *        description: MailSettingParams
+ *        type: object
+ *        properties:
  *          smtpHost:
  *            type: string
  *            description: host name of client's smtp server
@@ -115,16 +119,18 @@ module.exports = (crowi) => {
     siteUrlSetting: [
       body('siteUrl').trim().matches(/^(https?:\/\/[^/]+|)$/).isURL({ require_tld: false }),
     ],
+    fromAddress: [
+      body('fromAddress').trim().if(value => value !== '').isEmail(),
+    ],
     mailSetting: [
-      body('fromAddress').trim().isEmail(),
       body('smtpHost').trim(),
       body('smtpPort').trim().isPort(),
       body('smtpUser').trim(),
       body('smtpPassword').trim(),
     ],
     awsSetting: [
-      body('region').trim().matches(/^[a-z]+-[a-z]+-\d+$/).withMessage('リージョンには、AWSリージョン名を入力してください。 例: ap-northeast-1'),
-      body('customEndpoint').trim().matches(/^(https?:\/\/[^/]+|)$/).withMessage('カスタムエンドポイントは、http(s)://で始まるURLを指定してください。また、末尾の/は不要です。'),
+      body('region').trim().matches(/^[a-z]+-[a-z]+-\d+$/).withMessage((value, { req }) => req.t('validation.aws_region')),
+      body('customEndpoint').trim().matches(/^(https?:\/\/[^/]+|)$/).withMessage((value, { req }) => req.t('validation.aws_custom_endpoint')),
       body('bucket').trim(),
       body('accessKeyId').trim().matches(/^[\da-zA-Z]+$/),
       body('secretAccessKey').trim(),
@@ -292,7 +298,12 @@ module.exports = (crowi) => {
    * validate mail setting send test mail
    */
   async function validateMailSetting(req) {
-    const { mailService } = crowi;
+    const { configManager, mailService } = crowi;
+    const fromAddress = configManager.getConfig('crowi', 'mail:from');
+    if (fromAddress == null) {
+      throw Error('fromAddress is not setup');
+    }
+
     const option = {
       host: req.body.smtpHost,
       port: req.body.smtpPort,
@@ -311,7 +322,7 @@ module.exports = (crowi) => {
     debug('mailer setup for validate SMTP setting', smtpClient);
 
     const mailOptions = {
-      from: req.body.fromAddress,
+      from: fromAddress,
       to: req.user.email,
       subject: 'Wiki管理設定のアップデートによるメール通知',
       text: 'このメールは、WikiのSMTP設定のアップデートにより送信されています。',
@@ -333,7 +344,6 @@ module.exports = (crowi) => {
     mailService.publishUpdatedMessage();
 
     return {
-      fromAddress: configManager.getConfig('crowi', 'mail:from'),
       smtpHost: configManager.getConfig('crowi', 'mail:smtpHost'),
       smtpPort: configManager.getConfig('crowi', 'mail:smtpPort'),
       smtpUser: configManager.getConfig('crowi', 'mail:smtpUser'),
@@ -341,6 +351,44 @@ module.exports = (crowi) => {
     };
   };
 
+  /**
+   * @swagger
+   *
+   *    /app-settings/from-address:
+   *      put:
+   *        tags: [AppSettings]
+   *        operationId: updateAppSettingFromAddress
+   *        summary: /app-settings/from-address
+   *        description: Update from address
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/FromAddress'
+   *        responses:
+   *          200:
+   *            description: Succeeded to update from adress
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  $ref: '#/components/schemas/FromAddress'
+   */
+  router.put('/from-address', loginRequiredStrictly, adminRequired, csrf, validator.fromAddress, apiV3FormValidator, async(req, res) => {
+
+    try {
+      const mailSettingParams = await updateMailSettinConfig({ 'mail:from': req.body.fromAddress });
+
+      return res.apiv3({ mailSettingParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating from adress';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-from-adress-failed'));
+    }
+
+  });
+
   /**
    * @swagger
    *
@@ -377,7 +425,6 @@ module.exports = (crowi) => {
 
 
     const requestMailSettingParams = {
-      'mail:from': req.body.fromAddress,
       'mail:smtpHost': req.body.smtpHost,
       'mail:smtpPort': req.body.smtpPort,
       'mail:smtpUser': req.body.smtpUser,
@@ -420,7 +467,6 @@ module.exports = (crowi) => {
    */
   router.delete('/mail-setting', loginRequiredStrictly, adminRequired, csrf, async(req, res) => {
     const requestMailSettingParams = {
-      'mail:from': null,
       'mail:smtpHost': null,
       'mail:smtpPort': null,
       'mail:smtpUser': null,

+ 46 - 2
src/server/routes/apiv3/personal-setting.js

@@ -123,8 +123,52 @@ module.exports = (crowi) => {
    *                      description: personal params
    */
   router.get('/', accessTokenParser, loginRequiredStrictly, async(req, res) => {
-    const currentUser = await User.findUserByUsername(req.user.username);
-    return res.apiv3({ currentUser });
+    const { username } = req.user;
+
+    try {
+      const user = await User.findUserByUsername(username);
+      const currentUser = user.toObject();
+      return res.apiv3({ currentUser });
+
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err('update-personal-settings-failed');
+    }
+  });
+
+  /**
+   * @swagger
+   *
+   *    /personal-setting/is-password-set:
+   *      get:
+   *        tags: [PersonalSetting]
+   *        operationId: getIsPasswordSet
+   *        summary: /personal-setting
+   *        description: Get whether a password has been set
+   *        responses:
+   *          200:
+   *            description: Whether a password has been set
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    isPasswordSet:
+   *                      type: boolean
+   */
+  router.get('/is-password-set', accessTokenParser, loginRequiredStrictly, async(req, res) => {
+    const { username } = req.user;
+
+    try {
+      const user = await User.findUserByUsername(username);
+      const isPasswordSet = user.isPasswordSet();
+      return res.apiv3({ isPasswordSet });
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err('fail-to-get-whether-password-is-set');
+    }
+
   });
 
   /**

+ 2 - 1
src/server/routes/apiv3/users.js

@@ -119,7 +119,7 @@ module.exports = (crowi) => {
    *            description: page number
    *            schema:
    *              type: number
-   *          - name:  selectedStatusList
+   *          - name: selectedStatusList
    *            in: query
    *            description: status list
    *            schema:
@@ -184,6 +184,7 @@ module.exports = (crowi) => {
           sort: sortOutput,
           page,
           limit: PAGE_ITEMS,
+          select: User.USER_PUBLIC_FIELDS,
         },
       );
       return res.apiv3({ paginateResult });

+ 4 - 1
src/server/routes/attachment.js

@@ -228,7 +228,10 @@ module.exports = function(crowi, app) {
     }
     // reference
     else {
-      res.set('Content-Type', attachment.fileFormat);
+      res.set({
+        'Content-Type': attachment.fileFormat,
+        'Content-Security-Policy': "script-src 'unsafe-hashes'",
+      });
     }
   }
 

+ 0 - 1
src/server/routes/index.js

@@ -89,7 +89,6 @@ module.exports = function(crowi, app) {
   app.get('/admin/notification'              , loginRequiredStrictly , adminRequired , admin.notification.index);
   app.get('/admin/notification/slackAuth'    , loginRequiredStrictly , adminRequired , admin.notification.slackAuth);
   app.get('/admin/notification/slackSetting/disconnect', loginRequiredStrictly , adminRequired , admin.notification.disconnectFromSlack);
-  app.get('/_api/admin/users.search'         , loginRequiredStrictly , adminRequired , admin.api.usersSearch);
   app.get('/admin/global-notification/new'   , loginRequiredStrictly , adminRequired , admin.globalNotification.detail);
   app.get('/admin/global-notification/:id'   , loginRequiredStrictly , adminRequired , admin.globalNotification.detail);
 

+ 16 - 8
src/server/service/export.js

@@ -50,11 +50,17 @@ class ExportService {
    * @return {object} info for zip files and whether currentProgressingStatus exists
    */
   async getStatus() {
-    const zipFiles = fs.readdirSync(this.baseDir).filter((file) => { return path.extname(file) === '.zip' });
-    const zipFileStats = await Promise.all(zipFiles.map((file) => {
+    const zipFiles = fs.readdirSync(this.baseDir).filter(file => path.extname(file) === '.zip');
+
+    // process serially so as not to waste memory
+    const zipFileStats = [];
+    const parseZipFilePromises = zipFiles.map((file) => {
       const zipFile = this.getFile(file);
       return this.growiBridgeService.parseZipFile(zipFile);
-    }));
+    });
+    for await (const stat of parseZipFilePromises) {
+      zipFileStats.push(stat);
+    }
 
     // filter null object (broken zip)
     const filtered = zipFileStats.filter(element => element != null);
@@ -166,9 +172,7 @@ class ExportService {
     const collection = mongoose.connection.collection(collectionName);
 
     const nativeCursor = collection.find();
-    const readStream = nativeCursor
-      .snapshot()
-      .stream({ transform: JSON.stringify });
+    const readStream = nativeCursor.stream({ transform: JSON.stringify });
 
     // get TransformStream
     const transformStream = this.generateTransformStream();
@@ -201,8 +205,12 @@ class ExportService {
   async exportCollectionsToZippedJson(collections) {
     const metaJson = await this.createMetaJson();
 
-    const promises = collections.map(collectionName => this.exportCollectionToJson(collectionName));
-    const jsonFiles = await Promise.all(promises);
+    // process serially so as not to waste memory
+    const jsonFiles = [];
+    const jsonFilesPromises = collections.map(collectionName => this.exportCollectionToJson(collectionName));
+    for await (const jsonFile of jsonFilesPromises) {
+      jsonFiles.push(jsonFile);
+    }
 
     // send terminate event
     this.emitStartZippingEvent();

+ 26 - 18
src/server/service/import.js

@@ -128,10 +128,16 @@ class ImportService {
    */
   async getStatus() {
     const zipFiles = fs.readdirSync(this.baseDir).filter(file => path.extname(file) === '.zip');
-    const zipFileStats = await Promise.all(zipFiles.map((file) => {
+
+    // process serially so as not to waste memory
+    const zipFileStats = [];
+    const parseZipFilePromises = zipFiles.map((file) => {
       const zipFile = this.getFile(file);
       return this.growiBridgeService.parseZipFile(zipFile);
-    }));
+    });
+    for await (const stat of parseZipFilePromises) {
+      zipFileStats.push(stat);
+    }
 
     // filter null object (broken zip)
     const filtered = zipFileStats
@@ -158,23 +164,25 @@ class ImportService {
     // init status object
     this.currentProgressingStatus = new CollectionProgressingStatus(collections);
 
-    try {
-      const promises = collections.map((collectionName) => {
-        const importSettings = importSettingsMap[collectionName];
-        return this.importCollection(collectionName, importSettings);
-      });
-      await Promise.all(promises);
-    }
-    // catch ImportingCollectionError
-    catch (err) {
-      const { collectionProgress } = err;
-      logger.error(`failed to import to ${collectionProgress.collectionName}`, err);
-      this.emitProgressEvent(collectionProgress, { message: err.message });
-    }
-    finally {
-      this.currentProgressingStatus = null;
-      this.emitTerminateEvent();
+    // process serially so as not to waste memory
+    const promises = collections.map((collectionName) => {
+      const importSettings = importSettingsMap[collectionName];
+      return this.importCollection(collectionName, importSettings);
+    });
+    for await (const promise of promises) {
+      try {
+        await promise;
+      }
+      // catch ImportingCollectionError
+      catch (err) {
+        const { collectionProgress } = err;
+        logger.error(`failed to import to ${collectionProgress.collectionName}`, err);
+        this.emitProgressEvent(collectionProgress, { message: err.message });
+      }
     }
+
+    this.currentProgressingStatus = null;
+    this.emitTerminateEvent();
   }
 
   /**

+ 0 - 1
src/server/service/search-delegator/elasticsearch.js

@@ -378,7 +378,6 @@ class ElasticsearchDelegator {
         { path: 'creator', model: 'User', select: 'username' },
         { path: 'revision', model: 'Revision', select: 'body' },
       ])
-      .snapshot()
       .lean()
       .cursor();
 

+ 0 - 7
src/test/models/user.test.js

@@ -42,13 +42,6 @@ describe('User', () => {
         expect(user.name).toBe('Example for User Test');
       });
 
-      test('should be found by findUsersByPartOfEmail', async() => {
-        const users = await User.findUsersByPartOfEmail('usert', {});
-        expect(users).toBeInstanceOf(Array);
-        expect(users.length).toBe(2);
-        expect(users[0]).toBeInstanceOf(User);
-        expect(users[1]).toBeInstanceOf(User);
-      });
     });
   });
 

Разница между файлами не показана из-за своего большого размера
+ 396 - 215
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов