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

Merge branch 'master' into fix/20-basic-features

Shun Miyazawa 3 лет назад
Родитель
Сommit
c50d59966b
49 измененных файлов с 279 добавлено и 731 удалено
  1. 1 0
      package.json
  2. 0 1
      packages/app/_obsolete/src/client/nologin.jsx
  3. 1 1
      packages/app/cypress.config.ts
  4. 0 1
      packages/app/package.json
  5. 0 11
      packages/app/public/static/locales/en_US/admin.json
  6. 0 1
      packages/app/public/static/locales/en_US/translation.json
  7. 0 11
      packages/app/public/static/locales/ja_JP/admin.json
  8. 0 11
      packages/app/public/static/locales/zh_CN/admin.json
  9. 0 1
      packages/app/public/static/locales/zh_CN/translation.json
  10. 0 77
      packages/app/src/client/services/AdminBasicSecurityContainer.js
  11. 0 9
      packages/app/src/client/services/AdminGeneralSecurityContainer.js
  12. 1 4
      packages/app/src/client/services/AdminMarkDownContainer.js
  13. 11 4
      packages/app/src/client/services/page-operation.ts
  14. 9 8
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  15. 0 42
      packages/app/src/components/Admin/Security/BasicSecuritySetting.jsx
  16. 0 139
      packages/app/src/components/Admin/Security/BasicSecuritySettingContents.jsx
  17. 4 13
      packages/app/src/components/Admin/Security/SecurityManagementContents.jsx
  18. 1 1
      packages/app/src/components/Layout/BasicLayout.tsx
  19. 0 4
      packages/app/src/components/Layout/NoLoginLayout.module.scss
  20. 1 1
      packages/app/src/components/Layout/ShareLinkLayout.tsx
  21. 1 2
      packages/app/src/components/LoginForm.tsx
  22. 9 5
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  23. 1 1
      packages/app/src/components/Navbar/PersonalDropdown.jsx
  24. 3 3
      packages/app/src/components/PageEditor.tsx
  25. 0 112
      packages/app/src/components/PageEditor/Cheatsheet.jsx
  26. 114 0
      packages/app/src/components/PageEditor/Cheatsheet.tsx
  27. 0 8
      packages/app/src/components/PageEditor/Editor.module.scss
  28. 2 2
      packages/app/src/components/PageEditor/Editor.tsx
  29. 8 3
      packages/app/src/components/PageEditorByHackmd.tsx
  30. 5 3
      packages/app/src/components/PageStatusAlert.tsx
  31. 1 1
      packages/app/src/components/User/UserInfo.tsx
  32. 0 12
      packages/app/src/interfaces/activity.ts
  33. 5 0
      packages/app/src/interfaces/rehype.ts
  34. 3 1
      packages/app/src/interfaces/services/renderer.ts
  35. 25 0
      packages/app/src/migrations/20221219011829-remove-basic-auth-related-config.js
  36. 4 2
      packages/app/src/pages/[[...path]].page.tsx
  37. 0 4
      packages/app/src/pages/admin/security.page.tsx
  38. 0 1
      packages/app/src/pages/login.page.tsx
  39. 0 1
      packages/app/src/server/crowi/index.js
  40. 0 5
      packages/app/src/server/models/config.ts
  41. 8 8
      packages/app/src/server/routes/apiv3/markdown-setting.js
  42. 1 64
      packages/app/src/server/routes/apiv3/security-setting.js
  43. 0 1
      packages/app/src/server/routes/index.js
  44. 0 44
      packages/app/src/server/routes/login-passport.js
  45. 0 54
      packages/app/src/server/service/passport.ts
  46. 7 12
      packages/app/test/cypress/integration/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.spec.ts
  47. 14 12
      packages/app/test/cypress/integration/60-home/60-home--home.spec.ts
  48. 34 23
      packages/app/test/cypress/support/commands.ts
  49. 5 7
      yarn.lock

+ 1 - 0
package.json

@@ -62,6 +62,7 @@
     "@typescript-eslint/eslint-plugin": "^5.0.0",
     "@typescript-eslint/eslint-plugin": "^5.0.0",
     "@typescript-eslint/parser": "^5.0.0",
     "@typescript-eslint/parser": "^5.0.0",
     "cypress": "^12.0.1",
     "cypress": "^12.0.1",
+    "cypress-wait-until": "^1.7.2",
     "eslint": "^8.18.0",
     "eslint": "^8.18.0",
     "eslint-config-next": "^12.1.6",
     "eslint-config-next": "^12.1.6",
     "eslint-config-weseek": "^2.1.0",
     "eslint-config-weseek": "^2.1.0",

+ 0 - 1
packages/app/_obsolete/src/client/nologin.jsx

@@ -52,7 +52,6 @@ if (loginFormElem) {
     twitter: loginFormElem.dataset.isTwitterAuthEnabled === 'true',
     twitter: loginFormElem.dataset.isTwitterAuthEnabled === 'true',
     saml: loginFormElem.dataset.isSamlAuthEnabled === 'true',
     saml: loginFormElem.dataset.isSamlAuthEnabled === 'true',
     oidc: loginFormElem.dataset.isOidcAuthEnabled === 'true',
     oidc: loginFormElem.dataset.isOidcAuthEnabled === 'true',
-    basic: loginFormElem.dataset.isBasicAuthEnabled === 'true',
   };
   };
 
 
   Object.assign(componentMappings, {
   Object.assign(componentMappings, {

+ 1 - 1
packages/app/cypress.config.ts

@@ -16,6 +16,7 @@ export default defineConfig({
         return launchOptions;
         return launchOptions;
       });
       });
     },
     },
+    defaultCommandTimeout: 10000,
   },
   },
   fileServerFolder: 'test/cypress',
   fileServerFolder: 'test/cypress',
   fixturesFolder: 'test/cypress/fixtures',
   fixturesFolder: 'test/cypress/fixtures',
@@ -25,5 +26,4 @@ export default defineConfig({
   viewportWidth: 1400,
   viewportWidth: 1400,
   viewportHeight: 1024,
   viewportHeight: 1024,
 
 
-  defaultCommandTimeout: 30000,
 });
 });

+ 0 - 1
packages/app/package.json

@@ -146,7 +146,6 @@
     "passport": "^0.6.0",
     "passport": "^0.6.0",
     "passport-github": "^1.1.0",
     "passport-github": "^1.1.0",
     "passport-google-oauth20": "^2.0.0",
     "passport-google-oauth20": "^2.0.0",
-    "passport-http": "^0.3.0",
     "passport-ldapauth": "^3.0.1",
     "passport-ldapauth": "^3.0.1",
     "passport-local": "^1.0.0",
     "passport-local": "^1.0.0",
     "passport-saml": "^3.2.0",
     "passport-saml": "^3.2.0",

+ 0 - 11
packages/app/public/static/locales/en_US/admin.json

@@ -157,13 +157,6 @@
       "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "updated_saml": "Succeeded to update SAML setting"
       "updated_saml": "Succeeded to update SAML setting"
     },
     },
-    "Basic": {
-      "enable_basic": "Enable Basic",
-      "name": "Basic Authentication",
-      "desc_1": "Login with <code>username</code> in Authorization header.",
-      "desc_2": "User will be automatically generated if not exist.",
-      "updated_basic": "Succeeded to update Basic setting"
-    },
     "OAuth": {
     "OAuth": {
       "enable_oidc": "Enable OIDC",
       "enable_oidc": "Enable OIDC",
       "register": "Register for %s",
       "register": "Register for %s",
@@ -891,7 +884,6 @@
     "USER_LOGIN_WITH_TWITTER": "Login with Twitter",
     "USER_LOGIN_WITH_TWITTER": "Login with Twitter",
     "USER_LOGIN_WITH_OIDC": "Login with OIDC",
     "USER_LOGIN_WITH_OIDC": "Login with OIDC",
     "USER_LOGIN_WITH_SAML": "Login with SAML",
     "USER_LOGIN_WITH_SAML": "Login with SAML",
-    "USER_LOGIN_WITH_BASIC": "Login with BASIC",
     "USER_LOGIN_FAILURE": "Login failure",
     "USER_LOGIN_FAILURE": "Login failure",
     "USER_LOGOUT": "Logout",
     "USER_LOGOUT": "Logout",
     "USER_FOGOT_PASSWORD": "Request password reset",
     "USER_FOGOT_PASSWORD": "Request password reset",
@@ -969,9 +961,6 @@
     "ADMIN_AUTH_OIDC_ENABLED": "Enable OIDC auth",
     "ADMIN_AUTH_OIDC_ENABLED": "Enable OIDC auth",
     "ADMIN_AUTH_OIDC_DISABLED": "Disable OIDC auth",
     "ADMIN_AUTH_OIDC_DISABLED": "Disable OIDC auth",
     "ADMIN_AUTH_OIDC_UPDATE": "Update OIDC settings",
     "ADMIN_AUTH_OIDC_UPDATE": "Update OIDC settings",
-    "ADMIN_AUTH_BASIC_ENABLED": "Enable BASIC auth",
-    "ADMIN_AUTH_BASIC_DISABLED": "Disable BASIC auth",
-    "ADMIN_AUTH_BASIC_UPDATE": "Update BASIC auth settings",
     "ADMIN_AUTH_GOOGLE_ENABLED": "Enable Google auth",
     "ADMIN_AUTH_GOOGLE_ENABLED": "Enable Google auth",
     "ADMIN_AUTH_GOOGLE_DISABLED": "Disable Google auth",
     "ADMIN_AUTH_GOOGLE_DISABLED": "Disable Google auth",
     "ADMIN_AUTH_GOOGLE_UPDATE": "Update Google auth settings",
     "ADMIN_AUTH_GOOGLE_UPDATE": "Update Google auth settings",

+ 0 - 1
packages/app/public/static/locales/en_US/translation.json

@@ -114,7 +114,6 @@
   "external_account_management": "External Account Management",
   "external_account_management": "External Account Management",
   "UserGroup": "UserGroup",
   "UserGroup": "UserGroup",
   "Basic Settings": "Basic Settings",
   "Basic Settings": "Basic Settings",
-  "Basic authentication": "Basic authentication",
   "The contents entered here will be shown in the header etc": "The contents entered here will be shown in the header etc",
   "The contents entered here will be shown in the header etc": "The contents entered here will be shown in the header etc",
   "Public": "Public",
   "Public": "Public",
   "Anyone with the link": "Anyone with the link",
   "Anyone with the link": "Anyone with the link",

+ 0 - 11
packages/app/public/static/locales/ja_JP/admin.json

@@ -165,13 +165,6 @@
       "attr_based_login_control_rule_exampl2": "<h5>エスケープの例</h5>ルールに URL を利用したい場合は、次のようにエスケープしてください:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "attr_based_login_control_rule_exampl2": "<h5>エスケープの例</h5>ルールに URL を利用したい場合は、次のようにエスケープしてください:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "updated_saml": "Succeeded to update SAML setting"
       "updated_saml": "Succeeded to update SAML setting"
     },
     },
-    "Basic": {
-      "enable_basic": "Basic を有効にする",
-      "name": "Basic 認証",
-      "desc_1": "Authorization ヘッダに格納されている <code>username</code> でログインします。",
-      "desc_2": "ユーザーが存在しなかった場合は自動生成します。",
-      "updated_basic": "Basic認証 を更新しました"
-    },
     "OAuth": {
     "OAuth": {
       "enable_oidc": "OIDC を有効にする",
       "enable_oidc": "OIDC を有効にする",
       "register": "%sに登録",
       "register": "%sに登録",
@@ -899,7 +892,6 @@
     "USER_LOGIN_WITH_TWITTER": "Twitter 認証でログイン",
     "USER_LOGIN_WITH_TWITTER": "Twitter 認証でログイン",
     "USER_LOGIN_WITH_OIDC": "OIDC 認証でログイン",
     "USER_LOGIN_WITH_OIDC": "OIDC 認証でログイン",
     "USER_LOGIN_WITH_SAML": "SAML 認証でログイン",
     "USER_LOGIN_WITH_SAML": "SAML 認証でログイン",
-    "USER_LOGIN_WITH_BASIC": "BASIC 認証でログイン",
     "USER_LOGIN_FAILURE": "ログイン失敗",
     "USER_LOGIN_FAILURE": "ログイン失敗",
     "USER_LOGOUT": "ログアウト",
     "USER_LOGOUT": "ログアウト",
     "USER_FOGOT_PASSWORD": "パスワードリセットのリクエスト",
     "USER_FOGOT_PASSWORD": "パスワードリセットのリクエスト",
@@ -977,9 +969,6 @@
     "ADMIN_AUTH_OIDC_ENABLED": "OIDC 認証を有効",
     "ADMIN_AUTH_OIDC_ENABLED": "OIDC 認証を有効",
     "ADMIN_AUTH_OIDC_DISABLED": "OIDC 認証を無効",
     "ADMIN_AUTH_OIDC_DISABLED": "OIDC 認証を無効",
     "ADMIN_AUTH_OIDC_UPDATE": "OIDC 認証設定の更新",
     "ADMIN_AUTH_OIDC_UPDATE": "OIDC 認証設定の更新",
-    "ADMIN_AUTH_BASIC_ENABLED": "BASIC 認証の有効",
-    "ADMIN_AUTH_BASIC_DISABLED": "BASIC 認証の無効",
-    "ADMIN_AUTH_BASIC_UPDATE": "BASIC 認証設定の更新",
     "ADMIN_AUTH_GOOGLE_ENABLED": "Google 認証の有効",
     "ADMIN_AUTH_GOOGLE_ENABLED": "Google 認証の有効",
     "ADMIN_AUTH_GOOGLE_DISABLED": "Google 認証の無効",
     "ADMIN_AUTH_GOOGLE_DISABLED": "Google 認証の無効",
     "ADMIN_AUTH_GOOGLE_UPDATE": "Google 認証設定の更新",
     "ADMIN_AUTH_GOOGLE_UPDATE": "Google 認証設定の更新",

+ 0 - 11
packages/app/public/static/locales/zh_CN/admin.json

@@ -165,13 +165,6 @@
       "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "updated_saml": "Succeeded to update SAML setting"
       "updated_saml": "Succeeded to update SAML setting"
 		},
 		},
-		"Basic": {
-			"enable_basic": "Enable Basic",
-			"name": "Basic Authentication",
-			"desc_1": "Login with <code>username</code> in Authorization header.",
-			"desc_2": "User will be automatically generated if not exist.",
-			"updated_basic": "Succeeded to update Basic setting"
-		},
 		"OAuth": {
 		"OAuth": {
 			"enable_oidc": "Enable OIDC",
 			"enable_oidc": "Enable OIDC",
 			"register": "Register for %s",
 			"register": "Register for %s",
@@ -899,7 +892,6 @@
     "USER_LOGIN_WITH_TWITTER": "使用 Twitter 登录",
     "USER_LOGIN_WITH_TWITTER": "使用 Twitter 登录",
     "USER_LOGIN_WITH_OIDC": "使用 OIDC 登录",
     "USER_LOGIN_WITH_OIDC": "使用 OIDC 登录",
     "USER_LOGIN_WITH_SAML": "使用 SAML 登录",
     "USER_LOGIN_WITH_SAML": "使用 SAML 登录",
-    "USER_LOGIN_WITH_BASIC": "使用 BASIC 登录",
     "USER_LOGIN_FAILURE": "登录失败",
     "USER_LOGIN_FAILURE": "登录失败",
     "USER_LOGOUT": "注销",
     "USER_LOGOUT": "注销",
     "USER_FOGOT_PASSWORD": "要求重置密码",
     "USER_FOGOT_PASSWORD": "要求重置密码",
@@ -977,9 +969,6 @@
     "ADMIN_AUTH_OIDC_ENABLED": "启用 OIDC 身份验证",
     "ADMIN_AUTH_OIDC_ENABLED": "启用 OIDC 身份验证",
     "ADMIN_AUTH_OIDC_DISABLED": "禁用 OIDC 身份验证",
     "ADMIN_AUTH_OIDC_DISABLED": "禁用 OIDC 身份验证",
     "ADMIN_AUTH_OIDC_UPDATE": "更新 OIDC 设置",
     "ADMIN_AUTH_OIDC_UPDATE": "更新 OIDC 设置",
-    "ADMIN_AUTH_BASIC_ENABLED": "启用基本身份验证",
-    "ADMIN_AUTH_BASIC_DISABLED": "禁用基本身份验证",
-    "ADMIN_AUTH_BASIC_UPDATE": "更新基本认证设置",
     "ADMIN_AUTH_GOOGLE_ENABLED": "启用谷歌身份验证",
     "ADMIN_AUTH_GOOGLE_ENABLED": "启用谷歌身份验证",
     "ADMIN_AUTH_GOOGLE_DISABLED": "禁用谷歌身份验证",
     "ADMIN_AUTH_GOOGLE_DISABLED": "禁用谷歌身份验证",
     "ADMIN_AUTH_GOOGLE_UPDATE": "更新谷歌授权设置",
     "ADMIN_AUTH_GOOGLE_UPDATE": "更新谷歌授权设置",

+ 0 - 1
packages/app/public/static/locales/zh_CN/translation.json

@@ -121,7 +121,6 @@
   "UserGroup": "用户组",
   "UserGroup": "用户组",
   "ChildUserGroup": "儿童用户组",
   "ChildUserGroup": "儿童用户组",
 	"Basic Settings": "基础设置",
 	"Basic Settings": "基础设置",
-	"Basic authentication": "基本身份验证",
 	"The contents entered here will be shown in the header etc": "此处输入的内容将显示在标题等中",
 	"The contents entered here will be shown in the header etc": "此处输入的内容将显示在标题等中",
 	"Public": "公共",
 	"Public": "公共",
 	"Anyone with the link": "任何人",
 	"Anyone with the link": "任何人",

+ 0 - 77
packages/app/src/client/services/AdminBasicSecurityContainer.js

@@ -1,77 +0,0 @@
-import { isServer } from '@growi/core';
-import { Container } from 'unstated';
-
-import loggerFactory from '~/utils/logger';
-import { removeNullPropertyFromObject } from '~/utils/object-utils';
-
-import { apiv3Get, apiv3Put } from '../util/apiv3-client';
-
-const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
-
-/**
- * Service container for admin security page (BasicSecuritySetting.jsx)
- * @extends {Container} unstated Container
- */
-export default class AdminBasicSecurityContainer extends Container {
-
-  constructor() {
-    super();
-
-    if (isServer()) {
-      return;
-    }
-
-    this.state = {
-      isSameUsernameTreatedAsIdenticalUser: false,
-    };
-  }
-
-  /**
-   * retrieve security data
-   */
-  async retrieveSecurityData() {
-    try {
-      const response = await apiv3Get('/security-setting/');
-      const { basicAuth } = response.data.securityParams;
-      this.setState({
-        isSameUsernameTreatedAsIdenticalUser: basicAuth.isSameUsernameTreatedAsIdenticalUser,
-      });
-    }
-    catch (err) {
-      this.setState({ retrieveError: err });
-      logger.error(err);
-      throw new Error('Failed to fetch data');
-    }
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'AdminBasicSecurityContainer';
-  }
-
-  /**
-   * Switch isSameUsernameTreatedAsIdenticalUser
-   */
-  switchIsSameUsernameTreatedAsIdenticalUser() {
-    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
-  }
-
-  /**
-   * Update basicSetting
-   */
-  async updateBasicSetting() {
-    let requestParams = { isSameUsernameTreatedAsIdenticalUser: this.state.isSameUsernameTreatedAsIdenticalUser };
-
-    requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await apiv3Put('/security-setting/basic', requestParams);
-    const { securitySettingParams } = response.data;
-
-    this.setState({
-      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
-    });
-    return response;
-  }
-
-}

+ 0 - 9
packages/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -42,7 +42,6 @@ export default class AdminGeneralSecurityContainer extends Container {
       isLdapEnabled: false,
       isLdapEnabled: false,
       isSamlEnabled: false,
       isSamlEnabled: false,
       isOidcEnabled: false,
       isOidcEnabled: false,
-      isBasicEnabled: false,
       isGoogleEnabled: false,
       isGoogleEnabled: false,
       isGitHubEnabled: false,
       isGitHubEnabled: false,
       isTwitterEnabled: false,
       isTwitterEnabled: false,
@@ -82,7 +81,6 @@ export default class AdminGeneralSecurityContainer extends Container {
       isLdapEnabled: generalAuth.isLdapEnabled,
       isLdapEnabled: generalAuth.isLdapEnabled,
       isSamlEnabled: generalAuth.isSamlEnabled,
       isSamlEnabled: generalAuth.isSamlEnabled,
       isOidcEnabled: generalAuth.isOidcEnabled,
       isOidcEnabled: generalAuth.isOidcEnabled,
-      isBasicEnabled: generalAuth.isBasicEnabled,
       isGoogleEnabled: generalAuth.isGoogleEnabled,
       isGoogleEnabled: generalAuth.isGoogleEnabled,
       isGitHubEnabled: generalAuth.isGitHubEnabled,
       isGitHubEnabled: generalAuth.isGitHubEnabled,
       isTwitterEnabled: generalAuth.isTwitterEnabled,
       isTwitterEnabled: generalAuth.isTwitterEnabled,
@@ -318,13 +316,6 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.switchAuthentication('isOidcEnabled', 'oidc');
     this.switchAuthentication('isOidcEnabled', 'oidc');
   }
   }
 
 
-  /**
-   * Switch Basic enabled
-   */
-  async switchIsBasicEnabled() {
-    this.switchAuthentication('isBasicEnabled', 'basic');
-  }
-
   /**
   /**
    * Switch GoogleOAuth enabled
    * Switch GoogleOAuth enabled
    */
    */

+ 1 - 4
packages/app/src/client/services/AdminMarkDownContainer.js

@@ -27,7 +27,7 @@ export default class AdminMarkDownContainer extends Container {
       pageBreakSeparator: 1,
       pageBreakSeparator: 1,
       pageBreakCustomSeparator: '',
       pageBreakCustomSeparator: '',
       isEnabledXss: false,
       isEnabledXss: false,
-      xssOption: 1,
+      xssOption: '',
       tagWhiteList: '',
       tagWhiteList: '',
       attrWhiteList: '',
       attrWhiteList: '',
     };
     };
@@ -86,9 +86,6 @@ export default class AdminMarkDownContainer extends Container {
    * Switch enableXss
    * Switch enableXss
    */
    */
   switchEnableXss() {
   switchEnableXss() {
-    if (this.state.isEnabledXss) {
-      this.setState({ xssOption: null });
-    }
     this.setState({ isEnabledXss: !this.state.isEnabledXss });
     this.setState({ isEnabledXss: !this.state.isEnabledXss });
   }
   }
 
 

+ 11 - 4
packages/app/src/client/services/page-operation.ts

@@ -3,8 +3,8 @@ import urljoin from 'url-join';
 
 
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { useCurrentPageId } from '~/stores/context';
 import { useCurrentPageId } from '~/stores/context';
-import { useIsEnabledUnsavedWarning } from '~/stores/editor';
-import { useSWRxCurrentPage } from '~/stores/page';
+import { useIsEnabledUnsavedWarning, usePageTagsForEditors } from '~/stores/editor';
+import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -175,16 +175,23 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
   };
   };
 };
 };
 
 
-export const useUpdateStateAfterSave = () => {
+export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => Promise<void>) | undefined => {
   const { mutate: mutateCurrentPageId } = useCurrentPageId();
   const { mutate: mutateCurrentPageId } = useCurrentPageId();
   const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
+  const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
+  const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
+
+  if (pageId == null) { return }
 
 
   // update swr 'currentPageId', 'currentPage', remote states
   // update swr 'currentPageId', 'currentPage', remote states
-  return async(pageId: string) => {
+  return async() => {
     await mutateCurrentPageId(pageId);
     await mutateCurrentPageId(pageId);
     const updatedPage = await mutateCurrentPage();
     const updatedPage = await mutateCurrentPage();
 
 
+    await mutateTagsInfo(); // get from DB
+    syncTagsInfoForEditor(); // sync global state for client
+
     if (updatedPage == null) { return }
     if (updatedPage == null) { return }
 
 
     const remoterevisionData = {
     const remoterevisionData = {

+ 9 - 8
packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
 
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { RehypeSanitizeOption } from '~/interfaces/rehype';
 import { tags, attrs } from '~/services/xss/recommended-whitelist';
 import { tags, attrs } from '~/services/xss/recommended-whitelist';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -49,12 +50,12 @@ class XssForm extends React.Component {
               <input
               <input
                 type="radio"
                 type="radio"
                 className="custom-control-input"
                 className="custom-control-input"
-                id="xssOption2"
+                id="xssOption1"
                 name="XssOption"
                 name="XssOption"
-                checked={xssOption === 2}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: 2 }) }}
+                checked={xssOption === RehypeSanitizeOption.RECOMMENDED}
+                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeOption.RECOMMENDED }) }}
               />
               />
-              <label className="custom-control-label w-100" htmlFor="xssOption2">
+              <label className="custom-control-label w-100" htmlFor="xssOption1">
                 <p className="font-weight-bold">{t('markdown_settings.xss_options.recommended_setting')}</p>
                 <p className="font-weight-bold">{t('markdown_settings.xss_options.recommended_setting')}</p>
                 <div className="mt-4">
                 <div className="mt-4">
                   <div className="d-flex justify-content-between">
                   <div className="d-flex justify-content-between">
@@ -91,12 +92,12 @@ class XssForm extends React.Component {
               <input
               <input
                 type="radio"
                 type="radio"
                 className="custom-control-input"
                 className="custom-control-input"
-                id="xssOption3"
+                id="xssOption2"
                 name="XssOption"
                 name="XssOption"
-                checked={xssOption === 3}
-                onChange={() => { adminMarkDownContainer.setState({ xssOption: 3 }) }}
+                checked={xssOption === RehypeSanitizeOption.CUSTOM}
+                onChange={() => { adminMarkDownContainer.setState({ xssOption: RehypeSanitizeOption.CUSTOM }) }}
               />
               />
-              <label className="custom-control-label w-100" htmlFor="xssOption3">
+              <label className="custom-control-label w-100" htmlFor="xssOption2">
                 <p className="font-weight-bold">{t('markdown_settings.xss_options.custom_whitelist')}</p>
                 <p className="font-weight-bold">{t('markdown_settings.xss_options.custom_whitelist')}</p>
                 <WhiteListInput customizable />
                 <WhiteListInput customizable />
               </label>
               </label>

+ 0 - 42
packages/app/src/components/Admin/Security/BasicSecuritySetting.jsx

@@ -1,42 +0,0 @@
-import React, { useEffect, useCallback } from 'react';
-
-import PropTypes from 'prop-types';
-
-import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
-import { toastError } from '~/client/util/apiNotification';
-import { toArrayIfNot } from '~/utils/array-utils';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-import BasicSecurityManagementContents from './BasicSecuritySettingContents';
-
-const BasicSecurityManagement = (props) => {
-  const { adminBasicSecurityContainer } = props;
-
-  const fetchBasicSecuritySettingsData = useCallback(async() => {
-    try {
-      await adminBasicSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
-      const errs = toArrayIfNot(err);
-      toastError(errs);
-    }
-  }, [adminBasicSecurityContainer]);
-
-  useEffect(() => {
-    fetchBasicSecuritySettingsData();
-  }, [adminBasicSecurityContainer, fetchBasicSecuritySettingsData]);
-
-
-  return <BasicSecurityManagementContents />;
-};
-
-BasicSecurityManagement.propTypes = {
-  adminBasicSecurityContainer: PropTypes.instanceOf(AdminBasicSecurityContainer).isRequired,
-};
-
-const BasicSecurityManagementWithUnstatedContainer = withUnstatedContainers(BasicSecurityManagement, [
-  AdminBasicSecurityContainer,
-]);
-
-export default BasicSecurityManagementWithUnstatedContainer;

+ 0 - 139
packages/app/src/components/Admin/Security/BasicSecuritySettingContents.jsx

@@ -1,139 +0,0 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
-
-import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
-
-import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-class BasicSecurityManagementContents extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminBasicSecurityContainer, adminGeneralSecurityContainer } = this.props;
-
-    try {
-      await adminBasicSecurityContainer.updateBasicSetting();
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.Basic.updated_basic'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  render() {
-    const { t, adminGeneralSecurityContainer, adminBasicSecurityContainer } = this.props;
-    const { isBasicEnabled } = adminGeneralSecurityContainer.state;
-
-    return (
-      <React.Fragment>
-
-        <h2 className="alert-anchor border-bottom">
-          { t('security_settings.Basic.name') }
-        </h2>
-
-        {adminBasicSecurityContainer.state.retrieveError != null && (
-          <div className="alert alert-danger">
-            <p>{t('Error occurred')} : {adminBasicSecurityContainer.state.retrieveError}</p>
-          </div>
-        )}
-
-        <div className="form-group row">
-          <div className="col-6 offset-3">
-            <div className="custom-control custom-switch custom-checkbox-success">
-              <input
-                id="isBasicEnabled"
-                className="custom-control-input"
-                type="checkbox"
-                checked={adminGeneralSecurityContainer.state.isBasicEnabled}
-                onChange={() => { adminGeneralSecurityContainer.switchIsBasicEnabled() }}
-              />
-              <label className="custom-control-label" htmlFor="isBasicEnabled">
-                { t('security_settings.Basic.enable_basic') }
-              </label>
-            </div>
-            <p className="form-text text-muted">
-              <small>
-                <span dangerouslySetInnerHTML={{ __html: t('security_settings.Basic.desc_1') }} /><br />
-                { t('security_settings.Basic.desc_2')}
-              </small>
-            </p>
-            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('basic') && isBasicEnabled)
-            && <div className="badge badge-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
-          </div>
-        </div>
-
-        {isBasicEnabled && (
-          <React.Fragment>
-            <div className="row mb-5">
-              <div className="offset-md-3 col-md-6">
-                <div className="custom-control custom-checkbox custom-checkbox-success">
-                  <input
-                    id="bindByEmail-basic"
-                    className="custom-control-input"
-                    type="checkbox"
-                    checked={adminBasicSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
-                    onChange={() => { adminBasicSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
-                  />
-                  <label
-                    className="custom-control-label"
-                    htmlFor="bindByEmail-basic"
-                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical', 'username') }}
-                  />
-                </div>
-                <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn', 'username') }} />
-                </p>
-              </div>
-            </div>
-
-            <div className="row my-3">
-              <div className="offset-4 col-5">
-                <button
-                  type="button"
-                  className="btn btn-primary"
-                  disabled={adminBasicSecurityContainer.state.retrieveError != null}
-                  onClick={this.onClickSubmit}
-                >
-                  {t('Update')}
-                </button>
-              </div>
-            </div>
-
-          </React.Fragment>
-        )}
-
-      </React.Fragment>
-    );
-  }
-
-}
-
-BasicSecurityManagementContents.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
-  adminBasicSecurityContainer: PropTypes.instanceOf(AdminBasicSecurityContainer).isRequired,
-};
-
-const BasicSecurityManagementContentsWrapperFC = (props) => {
-  const { t } = useTranslation('admin');
-
-  return <BasicSecurityManagementContents t={t} {...props} />;
-};
-
-const BasicSecurityManagementContentsWrapper = withUnstatedContainers(BasicSecurityManagementContentsWrapperFC, [
-  AdminGeneralSecurityContainer,
-  AdminBasicSecurityContainer,
-]);
-
-export default BasicSecurityManagementContentsWrapper;

+ 4 - 13
packages/app/src/components/Admin/Security/SecurityManagementContents.jsx

@@ -6,7 +6,6 @@ import { TabContent, TabPane } from 'reactstrap';
 
 
 import CustomNav from '../../CustomNavigation/CustomNav';
 import CustomNav from '../../CustomNavigation/CustomNav';
 
 
-import BasicSecuritySetting from './BasicSecuritySetting';
 import FacebookSecuritySetting from './FacebookSecuritySetting';
 import FacebookSecuritySetting from './FacebookSecuritySetting';
 import GitHubSecuritySetting from './GitHubSecuritySetting';
 import GitHubSecuritySetting from './GitHubSecuritySetting';
 import GoogleSecuritySetting from './GoogleSecuritySetting';
 import GoogleSecuritySetting from './GoogleSecuritySetting';
@@ -51,30 +50,25 @@ const SecurityManagementContents = () => {
         i18n: 'OIDC',
         i18n: 'OIDC',
         index: 3,
         index: 3,
       },
       },
-      passport_basic: {
-        Icon: () => <i className="fa fa-lock" />,
-        i18n: 'BASIC',
-        index: 4,
-      },
       passport_google: {
       passport_google: {
         Icon: () => <i className="fa fa-google" />,
         Icon: () => <i className="fa fa-google" />,
         i18n: 'Google',
         i18n: 'Google',
-        index: 5,
+        index: 4,
       },
       },
       passport_github: {
       passport_github: {
         Icon: () => <i className="fa fa-github" />,
         Icon: () => <i className="fa fa-github" />,
         i18n: 'GitHub',
         i18n: 'GitHub',
-        index: 6,
+        index: 5,
       },
       },
       passport_twitter: {
       passport_twitter: {
         Icon: () => <i className="fa fa-twitter" />,
         Icon: () => <i className="fa fa-twitter" />,
         i18n: 'Twitter',
         i18n: 'Twitter',
-        index: 7,
+        index: 6,
       },
       },
       passport_facebook: {
       passport_facebook: {
         Icon: () => <i className="fa fa-facebook" />,
         Icon: () => <i className="fa fa-facebook" />,
         i18n: '(TBD) Facebook',
         i18n: '(TBD) Facebook',
-        index: 8,
+        index: 7,
       },
       },
     };
     };
   }, []);
   }, []);
@@ -126,9 +120,6 @@ const SecurityManagementContents = () => {
           <TabPane tabId="passport_oidc">
           <TabPane tabId="passport_oidc">
             {activeComponents.has('passport_oidc') && <OidcSecuritySetting />}
             {activeComponents.has('passport_oidc') && <OidcSecuritySetting />}
           </TabPane>
           </TabPane>
-          <TabPane tabId="passport_basic">
-            {activeComponents.has('passport_basic') && <BasicSecuritySetting />}
-          </TabPane>
           <TabPane tabId="passport_google">
           <TabPane tabId="passport_google">
             {activeComponents.has('passport_google') && <GoogleSecuritySetting />}
             {activeComponents.has('passport_google') && <GoogleSecuritySetting />}
           </TabPane>
           </TabPane>

+ 1 - 1
packages/app/src/components/Layout/BasicLayout.tsx

@@ -46,7 +46,7 @@ export const BasicLayout = ({
         <GrowiNavbar />
         <GrowiNavbar />
 
 
         <div className="page-wrapper d-flex d-print-block">
         <div className="page-wrapper d-flex d-print-block">
-          <div className="grw-sidebar-wrapper">
+          <div className="grw-sidebar-wrapper" data-testid="grw-sidebar-wrapper">
             <Sidebar />
             <Sidebar />
           </div>
           </div>
 
 

+ 0 - 4
packages/app/src/components/Layout/NoLoginLayout.module.scss

@@ -112,10 +112,6 @@
       rgba(#55a79a, 0.4),
       rgba(#55a79a, 0.4),
       $gray-700,
       $gray-700,
     ),
     ),
-    'basic': (
-      rgba(#24292e, 0.4),
-      $gray-700,
-    ),
   );
   );
 
 
   @each $label, $colors in $btn-fill-colors {
   @each $label, $colors in $btn-fill-colors {

+ 1 - 1
packages/app/src/components/Layout/ShareLinkLayout.tsx

@@ -30,7 +30,7 @@ export const ShareLinkLayout = ({
 
 
   return (
   return (
     <RawLayout title={title} className={myClassName}>
     <RawLayout title={title} className={myClassName}>
-      <GrowiNavbar />
+      <GrowiNavbar isGlobalSearchHidden={true} />
 
 
       <div className="page-wrapper d-flex d-print-block">
       <div className="page-wrapper d-flex d-print-block">
         <div className="flex-fill mw-0" style={{ position: 'relative' }}>
         <div className="flex-fill mw-0" style={{ position: 'relative' }}>

+ 1 - 2
packages/app/src/components/LoginForm.tsx

@@ -220,7 +220,6 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       twitter: 'twitter',
       twitter: 'twitter',
       oidc: 'openid',
       oidc: 'openid',
       saml: 'key',
       saml: 'key',
-      basic: 'lock',
     };
     };
 
 
     return (
     return (
@@ -497,7 +496,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   }
   }
 
 
   return (
   return (
-    <div className="noLogin-dialog mx-auto" id="noLogin-dialog">
+    <div className="noLogin-dialog mx-auto" id="noLogin-dialog" data-testid="login-form">
       <div className="row mx-0">
       <div className="row mx-0">
         <div className="col-12">
         <div className="col-12">
           <ReactCardFlip isFlipped={isRegistering} flipDirection="horizontal" cardZIndex="3">
           <ReactCardFlip isFlipped={isRegistering} flipDirection="horizontal" cardZIndex="3">

+ 9 - 5
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -188,7 +188,11 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
 
   const router = useRouter();
   const router = useRouter();
 
 
-  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { data: shareLinkId } = useShareLinkId();
+  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
+
+  const { data: currentPathname } = useCurrentPathname();
+  const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');
 
 
   const revision = currentPage?.revision;
   const revision = currentPage?.revision;
   const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined;
   const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined;
@@ -196,12 +200,10 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { data: isDrawerMode } = useDrawerMode();
   const { data: isDrawerMode } = useDrawerMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: pageId } = useCurrentPageId();
   const { data: pageId } = useCurrentPageId();
-  const { data: currentPathname } = useCurrentPathname();
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
-  const { data: shareLinkId } = useShareLinkId();
   const { data: isContainerFluid } = useIsContainerFluid();
   const { data: isContainerFluid } = useIsContainerFluid();
 
 
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
@@ -209,8 +211,10 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { data: isAbleToShowPageEditorModeManager } = useIsAbleToShowPageEditorModeManager();
   const { data: isAbleToShowPageEditorModeManager } = useIsAbleToShowPageEditorModeManager();
   const { data: isAbleToShowPageAuthors } = useIsAbleToShowPageAuthors();
   const { data: isAbleToShowPageAuthors } = useIsAbleToShowPageAuthors();
 
 
-  const { mutate: mutateSWRTagsInfo, data: tagsInfoData } = useSWRxTagsInfo(currentPage?._id);
-  const { data: tagsForEditors, mutate: mutatePageTagsForEditors, sync: syncPageTagsForEditors } = usePageTagsForEditors(currentPage?._id);
+  const { mutate: mutateSWRTagsInfo, data: tagsInfoData } = useSWRxTagsInfo(!isSharedPage ? currentPage?._id : undefined);
+
+  // eslint-disable-next-line max-len
+  const { data: tagsForEditors, mutate: mutatePageTagsForEditors, sync: syncPageTagsForEditors } = usePageTagsForEditors(!isSharedPage ? currentPage?._id : undefined);
 
 
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openRenameModal } = usePageRenameModal();

+ 1 - 1
packages/app/src/components/Navbar/PersonalDropdown.jsx

@@ -34,7 +34,7 @@ const PersonalDropdown = () => {
       {/* Button */}
       {/* Button */}
       {/* remove .dropdown-toggle for hide caret */}
       {/* remove .dropdown-toggle for hide caret */}
       {/* See https://stackoverflow.com/a/44577512/13183572 */}
       {/* See https://stackoverflow.com/a/44577512/13183572 */}
-      <button className="bg-transparent border-0 nav-link" type="button" ref={buttonRef} data-toggle="dropdown">
+      <button className="bg-transparent border-0 nav-link" type="button" ref={buttonRef} data-toggle="dropdown" data-testid="personal-dropdown-button">
         <UserPicture user={user} noLink noTooltip /><span className="ml-1 d-none d-lg-inline-block">&nbsp;{user.name}</span>
         <UserPicture user={user} noLink noTooltip /><span className="ml-1 d-none d-lg-inline-block">&nbsp;{user.name}</span>
       </button>
       </button>
 
 

+ 3 - 3
packages/app/src/components/PageEditor.tsx

@@ -93,7 +93,7 @@ const PageEditor = React.memo((): JSX.Element => {
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const saveOrUpdate = useSaveOrUpdate();
   const saveOrUpdate = useSaveOrUpdate();
 
 
-  const updateStateAfterSave = useUpdateStateAfterSave();
+  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
 
 
   const currentRevisionId = currentPage?.revision?._id;
   const currentRevisionId = currentPage?.revision?._id;
 
 
@@ -237,7 +237,7 @@ const PageEditor = React.memo((): JSX.Element => {
       await router.push(`/${page._id}`);
       await router.push(`/${page._id}`);
     }
     }
     else {
     else {
-      updateStateAfterSave(page._id);
+      updateStateAfterSave?.();
     }
     }
     mutateEditorMode(EditorMode.View);
     mutateEditorMode(EditorMode.View);
   }, [editorMode, save, isNotFound, mutateEditorMode, router, updateStateAfterSave]);
   }, [editorMode, save, isNotFound, mutateEditorMode, router, updateStateAfterSave]);
@@ -249,7 +249,7 @@ const PageEditor = React.memo((): JSX.Element => {
 
 
     const page = await save();
     const page = await save();
     if (page != null) {
     if (page != null) {
-      updateStateAfterSave(page._id);
+      updateStateAfterSave?.();
       toastSuccess(t('toaster.save_succeeded'));
       toastSuccess(t('toaster.save_succeeded'));
     }
     }
   }, [editorMode, save, t, updateStateAfterSave]);
   }, [editorMode, save, t, updateStateAfterSave]);

+ 0 - 112
packages/app/src/components/PageEditor/Cheatsheet.jsx

@@ -1,112 +0,0 @@
-/* eslint-disable max-len */
-
-import React from 'react';
-
-import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
-
-class Cheatsheet extends React.Component {
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <div className="row small">
-        <div className="col-sm-6">
-          <h4>{t('sandbox.header')}</h4>
-          <ul className="hljs">
-            <li><code># </code>{t('sandbox.header_x', { index: '1' })}</li>
-            <li><code>## </code>{t('sandbox.header_x', { index: '2' })}</li>
-            <li><code>### </code>{t('sandbox.header_x', { index: '3' })}</li>
-          </ul>
-          <h4>{t('sandbox.block')}</h4>
-          <p className="mb-1"><code>[{t('sandbox.empty_line')}]</code>{t('sandbox.block_detail')}</p>
-          <ul className="hljs">
-            <li>text</li>
-            <li></li>
-            <li>text</li>
-          </ul>
-          <h4>{t('sandbox.line_break')}</h4>
-          <p className="mb-1"><code>[ ][ ]</code> {t('sandbox.line_break_detail')}</p>
-          <ul className="hljs">
-            <li>text&nbsp;&nbsp;</li>
-            <li>text</li>
-          </ul>
-          <h4>{t('sandbox.typography')}</h4>
-          <ul className="hljs">
-            <li><i>*{t('sandbox.italics')}*</i></li>
-            <li><b>**{t('sandbox.bold')}**</b></li>
-            <li><i><b>***{t('sandbox.italic_bold')}***</b></i></li>
-            <li>~~{t('sandbox.strikethrough')}~~ =&lt; <s>{t('sandbox.strikethrough')}</s></li>
-          </ul>
-          <h4>{t('sandbox.link')}</h4>
-          <ul className="hljs">
-            <li>[Google](https://www.google.co.jp/)</li>
-            <li>[/Page1/ChildPage1]</li>
-          </ul>
-          <h4>{t('sandbox.code_highlight')}</h4>
-          <ul className="hljs">
-            <li>```javascript:index.js</li>
-            <li>writeCode();</li>
-            <li>```</li>
-          </ul>
-        </div>
-        <div className="col-sm-6">
-          <h4>{t('sandbox.list')}</h4>
-          <ul className="hljs">
-            <li>- {t('sandbox.unordered_list_x', { index: '1' })}</li>
-            <li>&nbsp;&nbsp;- {t('sandbox.unordered_list_x', { index: '1.1' })}</li>
-            <li>- {t('sandbox.unordered_list_x', { index: '2' })}</li>
-          </ul>
-          <ul className="hljs">
-            <li>1. {t('sandbox.ordered_list_x', { index: '1' })}</li>
-            <li>1. {t('sandbox.ordered_list_x', { index: '2' })}</li>
-          </ul>
-          <ul className="hljs">
-            <li>- [ ] {t('sandbox.task')}({t('sandbox.task_unchecked')})</li>
-            <li>- [x] {t('sandbox.task')}({t('sandbox.task_checked')})</li>
-          </ul>
-          <h4>{t('sandbox.quote')}</h4>
-          <ul className="hljs">
-            <li>&gt; {t('sandbox.quote1')}</li>
-            <li>&gt; {t('sandbox.quote2')}</li>
-          </ul>
-          <ul className="hljs">
-            <li>&gt;&gt; {t('sandbox.quote_nested')}</li>
-            <li>&gt;&gt;&gt; {t('sandbox.quote_nested')}</li>
-            <li>&gt;&gt;&gt;&gt; {t('sandbox.quote_nested')}</li>
-          </ul>
-          <h4>{t('sandbox.table')}</h4>
-          <pre className="border-0">
-            |Left&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;Mid&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Right|<br />
-            |:----------|:---------:|----------:|<br />
-            |col 1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;col 2&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;col 3|<br />
-            |col 1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;col 2&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;col 3|<br />
-          </pre>
-          <h4>{t('sandbox.image')}</h4>
-          <p className="mb-1"><code> ![{t('sandbox.alt_text')}](URL)</code> {t('sandbox.insert_image')}</p>
-          <ul className="hljs">
-            <li>![ex](https://example.com/image.png)</li>
-          </ul>
-
-          <hr />
-          <a href="/Sandbox" className="btn btn-info btn-block" target="_blank">
-            <i className="icon-share-alt" /> {t('sandbox.open_sandbox')}
-          </a>
-        </div>
-      </div>
-    );
-  }
-
-}
-
-Cheatsheet.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-};
-
-const CheatsheetWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <Cheatsheet t={t} {...props} />;
-};
-
-export default CheatsheetWrapperFC;

+ 114 - 0
packages/app/src/components/PageEditor/Cheatsheet.tsx

@@ -0,0 +1,114 @@
+/* eslint-disable max-len */
+
+import React from 'react';
+
+import { useTranslation } from 'next-i18next';
+import { PrismAsyncLight } from 'react-syntax-highlighter';
+import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
+
+export const Cheatsheet = (): JSX.Element => {
+  const { t } = useTranslation();
+
+  /*
+  * Each Element
+  */
+  // Left Side
+  const codeStr = `# ${t('sandbox.header_x', { index: '1' })}\n## ${t('sandbox.header_x', { index: '2' })}\n### ${t('sandbox.header_x', { index: '3' })}`;
+  const codeBlockStr = 'text\n\ntext';
+  const lineBlockStr = 'text\ntext';
+  const typographyStr = `*${t('sandbox.italics')}*\n**${t('sandbox.bold')}**\n***${t('sandbox.italic_bold')}***\n~~${t('sandbox.strikethrough')}~~`;
+  const linkStr = '[Google](https://www.google.co.jp/)\n[/Page1/ChildPage1]';
+  const codeHighlightStr = '```javascript:index.js\nwriteCode();\n```';
+
+  // Right Side
+  const codeListStr = `- ${t('sandbox.unordered_list_x', { index: '1' })}
+    - ${t('sandbox.unordered_list_x', { index: '1.1' })}
+    - ${t('sandbox.unordered_list_x', { index: '1.2' })}`;
+  const orderedListStr = `1. ${t('sandbox.ordered_list_x', { index: '1' })}\n1. ${t('sandbox.ordered_list_x', { index: '2' })}`;
+  const taskStr = `- [ ] ${t('sandbox.task')}(${t('sandbox.task_unchecked')})\n- [x] ${t('sandbox.task')}(${t('sandbox.task_checked')})`;
+  const quoteStr = `> ${t('sandbox.quote1')}\n> ${t('sandbox.quote2')}`;
+  const nestedQuoteStr = `>> ${t('sandbox.quote_nested')}\n>>> ${t('sandbox.quote_nested')}\n>>>> ${t('sandbox.quote_nested')}`;
+  const tableStr = '|Left       |    Mid    |      Right|\n|:----------|:---------:|----------:|\n|col 1      |   col 2   |      col 3|\n|col 1      |   col 2   |      col 3|';
+  const imageStr = '![ex](https://example.com/image.png)';
+
+
+  const renderCheetSheetElm = (CheetSheetElm: string) => {
+    return (
+      <PrismAsyncLight
+        className="code-highlighted"
+        PreTag="div"
+        style={oneDark}
+        language={'text'}
+      >
+        {String(CheetSheetElm).replace(/\n$/, '')}
+      </PrismAsyncLight>
+    );
+  };
+
+
+  return (
+    <div className="row small">
+      <div className="col-sm-6">
+
+        {/* Header */}
+        <h4>{t('sandbox.header')}</h4>
+        {renderCheetSheetElm(codeStr)}
+
+        {/* Block */}
+        <h4>{t('sandbox.block')}</h4>
+        <p className="mb-1"><code>[{t('sandbox.empty_line')}]</code>{t('sandbox.block_detail')}</p>
+        {renderCheetSheetElm(codeBlockStr)}
+
+        {/* Line Break */}
+        <h4>{t('sandbox.line_break')}</h4>
+        <p className="mb-1"><code>[ ][ ]</code> {t('sandbox.line_break_detail')}</p>
+        {renderCheetSheetElm(lineBlockStr)}
+
+
+        {/* Typography */}
+        <h4>{t('sandbox.typography')}</h4>
+        {renderCheetSheetElm(typographyStr)}
+
+        {/* Link */}
+        <h4>{t('sandbox.link')}</h4>
+        {renderCheetSheetElm(linkStr)}
+
+        {/* CodeHhighlight */}
+        <h4>{t('sandbox.code_highlight')}</h4>
+        {renderCheetSheetElm(codeHighlightStr)}
+      </div>
+
+      <div className="col-sm-6">
+        {/* List */}
+        <h4>{t('sandbox.list')}</h4>
+        {renderCheetSheetElm(codeListStr)}
+
+        {renderCheetSheetElm(orderedListStr)}
+
+        {renderCheetSheetElm(taskStr)}
+
+        {/* Quote */}
+        <h4>{t('sandbox.quote')}</h4>
+        {renderCheetSheetElm(quoteStr)}
+
+        {renderCheetSheetElm(nestedQuoteStr)}
+
+
+        {/* Table */}
+        <h4>{t('sandbox.table')}</h4>
+        {renderCheetSheetElm(tableStr)}
+
+        {/* Image */}
+        <h4>{t('sandbox.image')}</h4>
+        <p className="mb-1"><code> ![{t('sandbox.alt_text')}](URL)</code> {t('sandbox.insert_image')}</p>
+        {renderCheetSheetElm(imageStr)}
+
+        <hr />
+        <a href="/Sandbox" className="btn btn-info btn-block" target="_blank">
+          <i className="icon-share-alt" /> {t('sandbox.open_sandbox')}
+        </a>
+      </div>
+    </div>
+  );
+
+};

+ 0 - 8
packages/app/src/components/PageEditor/Editor.module.scss

@@ -154,11 +154,3 @@
     }
     }
   }
   }
 }
 }
-
-.modal-gfm-cheatsheet :global {
-  .modal-body {
-    .hljs {
-      font-family: var(--font-family-monospace);
-    }
-  }
-}

+ 2 - 2
packages/app/src/components/PageEditor/Editor.tsx

@@ -18,7 +18,7 @@ import { useIsMobile } from '~/stores/ui';
 import { IEditorMethods } from '../../interfaces/editor-methods';
 import { IEditorMethods } from '../../interfaces/editor-methods';
 
 
 import AbstractEditor from './AbstractEditor';
 import AbstractEditor from './AbstractEditor';
-import Cheatsheet from './Cheatsheet';
+import { Cheatsheet } from './Cheatsheet';
 import CodeMirrorEditor from './CodeMirrorEditor';
 import CodeMirrorEditor from './CodeMirrorEditor';
 import pasteHelper from './PasteHelper';
 import pasteHelper from './PasteHelper';
 import TextAreaEditor from './TextAreaEditor';
 import TextAreaEditor from './TextAreaEditor';
@@ -246,7 +246,7 @@ const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props
     };
     };
 
 
     return (
     return (
-      <Modal isOpen={isCheatsheetModalShown} toggle={hideCheatsheetModal} className={`${styles['modal-gfm-cheatsheet']}`} >
+      <Modal isOpen={isCheatsheetModalShown} toggle={hideCheatsheetModal} className={`modal-gfm-cheatsheet ${styles['modal-gfm-cheatsheet']}`} >
         <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
         <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
           <i className="icon-fw icon-question" />Markdown help
           <i className="icon-fw icon-question" />Markdown help
         </ModalHeader>
         </ModalHeader>

+ 8 - 3
packages/app/src/components/PageEditorByHackmd.tsx

@@ -85,7 +85,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { data: isHackmdDraftUpdatingInRealtime, mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
   const { data: isHackmdDraftUpdatingInRealtime, mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
   const { data: remoteRevisionId, mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
   const { data: remoteRevisionId, mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
 
 
-  const updateStateAfterSave = useUpdateStateAfterSave();
+  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
 
 
   const hackmdEditorRef = useRef<HackEditorRef>(null);
   const hackmdEditorRef = useRef<HackEditorRef>(null);
 
 
@@ -127,7 +127,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
         await router.push(`/${page._id}`);
         await router.push(`/${page._id}`);
       }
       }
       else {
       else {
-        updateStateAfterSave(page._id);
+        updateStateAfterSave?.();
       }
       }
       setIsInitialized(false);
       setIsInitialized(false);
       mutateEditorMode(EditorMode.View);
       mutateEditorMode(EditorMode.View);
@@ -162,6 +162,11 @@ export const PageEditorByHackmd = (): JSX.Element => {
     };
     };
   }, [resetInitializedStatusHandler]);
   }, [resetInitializedStatusHandler]);
 
 
+  useEffect(() => {
+    // for page translation: https://github.com/weseek/growi/pull/7100
+    setIsInitialized(false);
+  }, [pageId]);
+
 
 
   const isResume = useCallback(() => {
   const isResume = useCallback(() => {
     const isPageExistsOnHackmd = (pageIdOnHackmd != null);
     const isPageExistsOnHackmd = (pageIdOnHackmd != null);
@@ -251,7 +256,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       mutatePageData(res);
       mutatePageData(res);
 
 
       // set updated data
       // set updated data
-      updateStateAfterSave(res._id);
+      updateStateAfterSave?.();
       mutateTagsInfo();
       mutateTagsInfo();
 
 
       logger.debug('success to save');
       logger.debug('success to save');

+ 5 - 3
packages/app/src/components/PageStatusAlert.tsx

@@ -10,6 +10,7 @@ import {
 import { useConflictDiffModal } from '~/stores/modal';
 import { useConflictDiffModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/stores/remote-latest-page';
 import { useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/stores/remote-latest-page';
+import { EditorMode, useEditorMode } from '~/stores/ui';
 
 
 import { Username } from './User/Username';
 import { Username } from './User/Username';
 
 
@@ -29,6 +30,7 @@ export const PageStatusAlert = (): JSX.Element => {
   const { data: isConflict } = useIsConflict();
   const { data: isConflict } = useIsConflict();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { open: openConflictDiffModal } = useConflictDiffModal();
   const { open: openConflictDiffModal } = useConflictDiffModal();
+  const { mutate: mutateEditorMode } = useEditorMode();
 
 
   // store remote latest page data
   // store remote latest page data
   const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
@@ -72,12 +74,12 @@ export const PageStatusAlert = (): JSX.Element => {
           {t('hackmd.this_page_has_draft')}
           {t('hackmd.this_page_has_draft')}
         </>,
         </>,
       btn:
       btn:
-        <a href="#hackmd" key="btnOpenHackmdPageHasDraft" className="btn btn-outline-white">
+        <button onClick={() => mutateEditorMode(EditorMode.HackMD)} className="btn btn-outline-white">
           <i className="fa fa-fw fa-file-text-o mr-1"></i>
           <i className="fa fa-fw fa-file-text-o mr-1"></i>
           Open HackMD Editor
           Open HackMD Editor
-        </a>,
+        </button>,
     };
     };
-  }, [t]);
+  }, [mutateEditorMode, t]);
 
 
   const getContentsForUpdatedAlert = useCallback((): AlertComponentContents => {
   const getContentsForUpdatedAlert = useCallback((): AlertComponentContents => {
 
 

+ 1 - 1
packages/app/src/components/User/UserInfo.tsx

@@ -19,7 +19,7 @@ export const UserInfo = (props: UserInfoProps): JSX.Element => {
   }
   }
 
 
   return (
   return (
-    <div className={`${styles['grw-users-info']} grw-users-info d-flex align-items-center d-edit-none mb-5 pb-3 border-bottom`}>
+    <div className={`${styles['grw-users-info']} grw-users-info d-flex align-items-center d-edit-none mb-5 pb-3 border-bottom`} data-testid="grw-users-info">
       <UserPicture user={author} />
       <UserPicture user={author} />
       <div className="users-meta">
       <div className="users-meta">
         <h1 className="user-page-name">
         <h1 className="user-page-name">

+ 0 - 12
packages/app/src/interfaces/activity.ts

@@ -16,7 +16,6 @@ const ACTION_USER_LOGIN_WITH_GITHUB = 'USER_LOGIN_WITH_GITHUB';
 const ACTION_USER_LOGIN_WITH_TWITTER = 'USER_LOGIN_WITH_TWITTER';
 const ACTION_USER_LOGIN_WITH_TWITTER = 'USER_LOGIN_WITH_TWITTER';
 const ACTION_USER_LOGIN_WITH_OIDC = 'USER_LOGIN_WITH_OIDC';
 const ACTION_USER_LOGIN_WITH_OIDC = 'USER_LOGIN_WITH_OIDC';
 const ACTION_USER_LOGIN_WITH_SAML = 'USER_LOGIN_WITH_SAML';
 const ACTION_USER_LOGIN_WITH_SAML = 'USER_LOGIN_WITH_SAML';
-const ACTION_USER_LOGIN_WITH_BASIC = 'USER_LOGIN_WITH_BASIC';
 const ACTION_USER_LOGIN_FAILURE = 'USER_LOGIN_FAILURE';
 const ACTION_USER_LOGIN_FAILURE = 'USER_LOGIN_FAILURE';
 const ACTION_USER_LOGOUT = 'USER_LOGOUT';
 const ACTION_USER_LOGOUT = 'USER_LOGOUT';
 const ACTION_USER_FOGOT_PASSWORD = 'USER_FOGOT_PASSWORD';
 const ACTION_USER_FOGOT_PASSWORD = 'USER_FOGOT_PASSWORD';
@@ -94,9 +93,6 @@ const ACTION_ADMIN_AUTH_SAML_UPDATE = 'ADMIN_AUTH_SAML_UPDATE';
 const ACTION_ADMIN_AUTH_OIDC_ENABLED = 'ADMIN_AUTH_OIDC_ENABLED';
 const ACTION_ADMIN_AUTH_OIDC_ENABLED = 'ADMIN_AUTH_OIDC_ENABLED';
 const ACTION_ADMIN_AUTH_OIDC_DISABLED = 'ADMIN_AUTH_OIDC_DISABLED';
 const ACTION_ADMIN_AUTH_OIDC_DISABLED = 'ADMIN_AUTH_OIDC_DISABLED';
 const ACTION_ADMIN_AUTH_OIDC_UPDATE = 'ADMIN_AUTH_OIDC_UPDATE';
 const ACTION_ADMIN_AUTH_OIDC_UPDATE = 'ADMIN_AUTH_OIDC_UPDATE';
-const ACTION_ADMIN_AUTH_BASIC_ENABLED = 'ADMIN_AUTH_BASIC_ENABLED';
-const ACTION_ADMIN_AUTH_BASIC_DISABLED = 'ADMIN_AUTH_BASIC_DISABLED';
-const ACTION_ADMIN_AUTH_BASIC_UPDATE = 'ADMIN_AUTH_BASIC_UPDATE';
 const ACTION_ADMIN_AUTH_GOOGLE_ENABLED = 'ADMIN_AUTH_GOOGLE_ENABLED';
 const ACTION_ADMIN_AUTH_GOOGLE_ENABLED = 'ADMIN_AUTH_GOOGLE_ENABLED';
 const ACTION_ADMIN_AUTH_GOOGLE_DISABLED = 'ADMIN_AUTH_GOOGLE_DISABLED';
 const ACTION_ADMIN_AUTH_GOOGLE_DISABLED = 'ADMIN_AUTH_GOOGLE_DISABLED';
 const ACTION_ADMIN_AUTH_GOOGLE_UPDATE = 'ADMIN_AUTH_GOOGLE_UPDATE';
 const ACTION_ADMIN_AUTH_GOOGLE_UPDATE = 'ADMIN_AUTH_GOOGLE_UPDATE';
@@ -199,7 +195,6 @@ export const SupportedAction = {
   ACTION_USER_LOGIN_WITH_TWITTER,
   ACTION_USER_LOGIN_WITH_TWITTER,
   ACTION_USER_LOGIN_WITH_OIDC,
   ACTION_USER_LOGIN_WITH_OIDC,
   ACTION_USER_LOGIN_WITH_SAML,
   ACTION_USER_LOGIN_WITH_SAML,
-  ACTION_USER_LOGIN_WITH_BASIC,
   ACTION_USER_LOGIN_FAILURE,
   ACTION_USER_LOGIN_FAILURE,
   ACTION_USER_LOGOUT,
   ACTION_USER_LOGOUT,
   ACTION_USER_FOGOT_PASSWORD,
   ACTION_USER_FOGOT_PASSWORD,
@@ -277,9 +272,6 @@ export const SupportedAction = {
   ACTION_ADMIN_AUTH_OIDC_ENABLED,
   ACTION_ADMIN_AUTH_OIDC_ENABLED,
   ACTION_ADMIN_AUTH_OIDC_DISABLED,
   ACTION_ADMIN_AUTH_OIDC_DISABLED,
   ACTION_ADMIN_AUTH_OIDC_UPDATE,
   ACTION_ADMIN_AUTH_OIDC_UPDATE,
-  ACTION_ADMIN_AUTH_BASIC_ENABLED,
-  ACTION_ADMIN_AUTH_BASIC_DISABLED,
-  ACTION_ADMIN_AUTH_BASIC_UPDATE,
   ACTION_ADMIN_AUTH_GOOGLE_ENABLED,
   ACTION_ADMIN_AUTH_GOOGLE_ENABLED,
   ACTION_ADMIN_AUTH_GOOGLE_DISABLED,
   ACTION_ADMIN_AUTH_GOOGLE_DISABLED,
   ACTION_ADMIN_AUTH_GOOGLE_UPDATE,
   ACTION_ADMIN_AUTH_GOOGLE_UPDATE,
@@ -383,7 +375,6 @@ export const SmallActionGroup = {
   ACTION_USER_LOGIN_WITH_TWITTER,
   ACTION_USER_LOGIN_WITH_TWITTER,
   ACTION_USER_LOGIN_WITH_OIDC,
   ACTION_USER_LOGIN_WITH_OIDC,
   ACTION_USER_LOGIN_WITH_SAML,
   ACTION_USER_LOGIN_WITH_SAML,
-  ACTION_USER_LOGIN_WITH_BASIC,
   ACTION_USER_LOGIN_FAILURE,
   ACTION_USER_LOGIN_FAILURE,
   ACTION_USER_LOGOUT,
   ACTION_USER_LOGOUT,
   ACTION_PAGE_CREATE,
   ACTION_PAGE_CREATE,
@@ -468,9 +459,6 @@ export const LargeActionGroup = {
   ACTION_ADMIN_AUTH_OIDC_ENABLED,
   ACTION_ADMIN_AUTH_OIDC_ENABLED,
   ACTION_ADMIN_AUTH_OIDC_DISABLED,
   ACTION_ADMIN_AUTH_OIDC_DISABLED,
   ACTION_ADMIN_AUTH_OIDC_UPDATE,
   ACTION_ADMIN_AUTH_OIDC_UPDATE,
-  ACTION_ADMIN_AUTH_BASIC_ENABLED,
-  ACTION_ADMIN_AUTH_BASIC_DISABLED,
-  ACTION_ADMIN_AUTH_BASIC_UPDATE,
   ACTION_ADMIN_AUTH_GOOGLE_ENABLED,
   ACTION_ADMIN_AUTH_GOOGLE_ENABLED,
   ACTION_ADMIN_AUTH_GOOGLE_DISABLED,
   ACTION_ADMIN_AUTH_GOOGLE_DISABLED,
   ACTION_ADMIN_AUTH_GOOGLE_UPDATE,
   ACTION_ADMIN_AUTH_GOOGLE_UPDATE,

+ 5 - 0
packages/app/src/interfaces/rehype.ts

@@ -4,3 +4,8 @@ export const RehypeSanitizeOption = {
 } as const;
 } as const;
 
 
 export type RehypeSanitizeOption = typeof RehypeSanitizeOption[keyof typeof RehypeSanitizeOption];
 export type RehypeSanitizeOption = typeof RehypeSanitizeOption[keyof typeof RehypeSanitizeOption];
+
+export type RehypeSanitizeOptionConfig = {
+  isEnabledXssPrevention: boolean,
+  // Todo add types for custom sanitize option at https://redmine.weseek.co.jp/issues/109763
+}

+ 3 - 1
packages/app/src/interfaces/services/renderer.ts

@@ -1,5 +1,7 @@
 import { XssOptionConfig } from '~/services/xss/xssOption';
 import { XssOptionConfig } from '~/services/xss/xssOption';
 
 
+import { RehypeSanitizeOptionConfig } from '../rehype';
+
 
 
 export type RendererConfig = {
 export type RendererConfig = {
   isEnabledLinebreaks: boolean,
   isEnabledLinebreaks: boolean,
@@ -10,4 +12,4 @@ export type RendererConfig = {
 
 
   plantumlUri: string | null,
   plantumlUri: string | null,
   blockdiagUri: string | null,
   blockdiagUri: string | null,
-} & XssOptionConfig;
+} & XssOptionConfig & RehypeSanitizeOptionConfig;

+ 25 - 0
packages/app/src/migrations/20221219011829-remove-basic-auth-related-config.js

@@ -0,0 +1,25 @@
+// eslint-disable-next-line import/no-named-as-default
+import Config from '~/server/models/config';
+import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:remove-basic-auth-related-config');
+
+const mongoose = require('mongoose');
+
+module.exports = {
+  async up() {
+    logger.info('Apply migration');
+    mongoose.connect(getMongoUri(), mongoOptions);
+
+    await Config.findOneAndDelete({ key: 'security:passport-basic:isEnabled' });
+    await Config.findOneAndDelete({ key: 'security:passport-basic:isSameUsernameTreatedAsIdenticalUser' });
+
+    logger.info('Migration has successfully applied');
+  },
+
+  async down() {
+    // No rollback
+  },
+};

+ 4 - 2
packages/app/src/pages/[[...path]].page.tsx

@@ -262,7 +262,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   useRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
   useRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
   useRemoteRevisionId(pageWithMeta?.data.revision._id);
   useRemoteRevisionId(pageWithMeta?.data.revision._id);
   usePageIdOnHackmd(pageWithMeta?.data.pageIdOnHackmd);
   usePageIdOnHackmd(pageWithMeta?.data.pageIdOnHackmd);
-  useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd);
+  useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd ?? false);
   // useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical
   // useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical
   useCurrentPathname(props.currentPathname);
   useCurrentPathname(props.currentPathname);
 
 
@@ -571,10 +571,12 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     blockdiagUri: process.env.BLOCKDIAG_URI ?? null,
     blockdiagUri: process.env.BLOCKDIAG_URI ?? null,
 
 
     // XSS Options
     // XSS Options
-    isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
     attrWhiteList: crowi.xssService.getAttrWhiteList(),
     attrWhiteList: crowi.xssService.getAttrWhiteList(),
     tagWhiteList: crowi.xssService.getTagWhiteList(),
     tagWhiteList: crowi.xssService.getTagWhiteList(),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+
+    // XSS: rehype-sanitize options
+    isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
   };
   };
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {

+ 0 - 4
packages/app/src/pages/admin/security.page.tsx

@@ -6,8 +6,6 @@ import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { Container, Provider } from 'unstated';
 import { Container, Provider } from 'unstated';
 
 
-
-import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
@@ -50,7 +48,6 @@ const AdminSecuritySettingsPage: NextPage<Props> = (props) => {
       const adminLdapSecurityContainer = new AdminLdapSecurityContainer();
       const adminLdapSecurityContainer = new AdminLdapSecurityContainer();
       const adminSamlSecurityContainer = new AdminSamlSecurityContainer();
       const adminSamlSecurityContainer = new AdminSamlSecurityContainer();
       const adminOidcSecurityContainer = new AdminOidcSecurityContainer();
       const adminOidcSecurityContainer = new AdminOidcSecurityContainer();
-      const adminBasicSecurityContainer = new AdminBasicSecurityContainer();
       const adminGoogleSecurityContainer = new AdminGoogleSecurityContainer();
       const adminGoogleSecurityContainer = new AdminGoogleSecurityContainer();
       const adminGitHubSecurityContainer = new AdminGitHubSecurityContainer();
       const adminGitHubSecurityContainer = new AdminGitHubSecurityContainer();
       const adminTwitterSecurityContainer = new AdminTwitterSecurityContainer();
       const adminTwitterSecurityContainer = new AdminTwitterSecurityContainer();
@@ -61,7 +58,6 @@ const AdminSecuritySettingsPage: NextPage<Props> = (props) => {
         adminLdapSecurityContainer,
         adminLdapSecurityContainer,
         adminSamlSecurityContainer,
         adminSamlSecurityContainer,
         adminOidcSecurityContainer,
         adminOidcSecurityContainer,
-        adminBasicSecurityContainer,
         adminGoogleSecurityContainer,
         adminGoogleSecurityContainer,
         adminGitHubSecurityContainer,
         adminGitHubSecurityContainer,
         adminTwitterSecurityContainer,
         adminTwitterSecurityContainer,

+ 0 - 1
packages/app/src/pages/login.page.tsx

@@ -87,7 +87,6 @@ function injectEnabledStrategies(context: GetServerSidePropsContext, props: Prop
     twitter: configManager.getConfig('crowi', 'security:passport-twitter:isEnabled'),
     twitter: configManager.getConfig('crowi', 'security:passport-twitter:isEnabled'),
     saml: configManager.getConfig('crowi', 'security:passport-saml:isEnabled'),
     saml: configManager.getConfig('crowi', 'security:passport-saml:isEnabled'),
     oidc: configManager.getConfig('crowi', 'security:passport-oidc:isEnabled'),
     oidc: configManager.getConfig('crowi', 'security:passport-oidc:isEnabled'),
-    basic: configManager.getConfig('crowi', 'security:passport-basic:isEnabled'),
   };
   };
 
 
   props.enabledStrategies = enabledStrategies;
   props.enabledStrategies = enabledStrategies;

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

@@ -354,7 +354,6 @@ Crowi.prototype.setupPassport = async function() {
     this.passportService.setupStrategyById('ldap');
     this.passportService.setupStrategyById('ldap');
     this.passportService.setupStrategyById('saml');
     this.passportService.setupStrategyById('saml');
     this.passportService.setupStrategyById('oidc');
     this.passportService.setupStrategyById('oidc');
-    this.passportService.setupStrategyById('basic');
     this.passportService.setupStrategyById('google');
     this.passportService.setupStrategyById('google');
     this.passportService.setupStrategyById('github');
     this.passportService.setupStrategyById('github');
     this.passportService.setupStrategyById('twitter');
     this.passportService.setupStrategyById('twitter');

+ 0 - 5
packages/app/src/server/models/config.ts

@@ -102,9 +102,6 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
 
 
   'security:passport-oidc:isEnabled' : false,
   'security:passport-oidc:isEnabled' : false,
 
 
-  'security:passport-basic:isEnabled' : false,
-  'security:passport-basic:isSameUsernameTreatedAsIdenticalUser': false,
-
   'aws:s3Bucket'          : 'growi',
   'aws:s3Bucket'          : 'growi',
   'aws:s3Region'          : 'ap-northeast-1',
   'aws:s3Region'          : 'ap-northeast-1',
   'aws:s3AccessKeyId'     : undefined,
   'aws:s3AccessKeyId'     : undefined,
@@ -150,8 +147,6 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
 };
 };
 
 
 export const defaultMarkdownConfigs: { [key: string]: any } = {
 export const defaultMarkdownConfigs: { [key: string]: any } = {
-  'markdown:xss:isEnabledPrevention': true,
-  'markdown:xss:option': 2,
   'markdown:xss:tagWhiteList': [],
   'markdown:xss:tagWhiteList': [],
   'markdown:xss:attrWhiteList': [],
   'markdown:xss:attrWhiteList': [],
   'markdown:rehypeSanitize:isEnabledPrevention': true,
   'markdown:rehypeSanitize:isEnabledPrevention': true,

+ 8 - 8
packages/app/src/server/routes/apiv3/markdown-setting.js

@@ -125,8 +125,8 @@ module.exports = (crowi) => {
       isIndentSizeForced: await crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
       isIndentSizeForced: await crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
       pageBreakSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
       pageBreakSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
       pageBreakCustomSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
       pageBreakCustomSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
-      isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
-      xssOption: await crowi.configManager.getConfig('markdown', 'markdown:xss:option'),
+      isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
+      xssOption: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
       tagWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:tagWhiteList'),
       tagWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:tagWhiteList'),
       attrWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:attrWhiteList'),
       attrWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:attrWhiteList'),
     };
     };
@@ -293,17 +293,17 @@ module.exports = (crowi) => {
     }
     }
 
 
     const reqestXssParams = {
     const reqestXssParams = {
-      'markdown:xss:isEnabledPrevention': req.body.isEnabledXss,
-      'markdown:xss:option': req.body.xssOption,
-      'markdown:xss:tagWhiteList': req.body.tagWhiteList,
-      'markdown:xss:attrWhiteList': req.body.attrWhiteList,
+      'markdown:rehypeSanitize:isEnabledPrevention': req.body.isEnabledXss,
+      'markdown:rehypeSanitize:option': req.body.xssOption,
+      'markdown:xss:tagWhiteList': req.body.tagWhiteList, // Todo: need to be changed at https://redmine.weseek.co.jp/issues/109763
+      'markdown:xss:attrWhiteList': req.body.attrWhiteList, // Todo: need to be changed at https://redmine.weseek.co.jp/issues/109763
     };
     };
 
 
     try {
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('markdown', reqestXssParams);
       await crowi.configManager.updateConfigsInTheSameNamespace('markdown', reqestXssParams);
       const xssParams = {
       const xssParams = {
-        isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
-        xssOption: await crowi.configManager.getConfig('markdown', 'markdown:xss:option'),
+        isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:isEnabledPrevention'),
+        xssOption: await crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:option'),
         tagWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:tagWhiteList'),
         tagWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:tagWhiteList'),
         attrWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:attrWhiteList'),
         attrWhiteList: await crowi.configManager.getConfig('markdown', 'markdown:xss:attrWhiteList'),
       };
       };

+ 1 - 64
packages/app/src/server/routes/apiv3/security-setting.js

@@ -34,7 +34,7 @@ const validator = {
   authenticationSetting: [
   authenticationSetting: [
     body('isEnabled').if(value => value != null).isBoolean(),
     body('isEnabled').if(value => value != null).isBoolean(),
     body('authId').isString().isIn([
     body('authId').isString().isIn([
-      'local', 'ldap', 'saml', 'oidc', 'basic', 'google', 'github', 'twitter',
+      'local', 'ldap', 'saml', 'oidc', 'google', 'github', 'twitter',
     ]),
     ]),
   ],
   ],
   localSetting: [
   localSetting: [
@@ -91,9 +91,6 @@ const validator = {
     body('isSameUsernameTreatedAsIdenticalUser').if(value => value != null).isBoolean(),
     body('isSameUsernameTreatedAsIdenticalUser').if(value => value != null).isBoolean(),
     body('isSameEmailTreatedAsIdenticalUser').if(value => value != null).isBoolean(),
     body('isSameEmailTreatedAsIdenticalUser').if(value => value != null).isBoolean(),
   ],
   ],
-  basicAuth: [
-    body('isSameUsernameTreatedAsIdenticalUser').if(value => value != null).isBoolean(),
-  ],
   googleOAuth: [
   googleOAuth: [
     body('googleClientId').if(value => value != null).isString(),
     body('googleClientId').if(value => value != null).isString(),
     body('googleClientSecret').if(value => value != null).isString(),
     body('googleClientSecret').if(value => value != null).isString(),
@@ -291,12 +288,6 @@ const validator = {
  *          isSameEmailTreatedAsIdenticalUser:
  *          isSameEmailTreatedAsIdenticalUser:
  *            type: boolean
  *            type: boolean
  *            description: local account automatically linked the email matched
  *            description: local account automatically linked the email matched
- *      BasicAuthSetting:
- *        type: object
- *        properties:
- *          isSameUsernameTreatedAsIdenticalUser:
- *            type: boolean
- *            description: local account automatically linked the email matched
  *      GitHubOAuthSetting:
  *      GitHubOAuthSetting:
  *        type: object
  *        type: object
  *        properties:
  *        properties:
@@ -398,7 +389,6 @@ module.exports = (crowi) => {
         isLdapEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:isEnabled'),
         isLdapEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-ldap:isEnabled'),
         isSamlEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-saml:isEnabled'),
         isSamlEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-saml:isEnabled'),
         isOidcEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:isEnabled'),
         isOidcEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:isEnabled'),
-        isBasicEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-basic:isEnabled'),
         isGoogleEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-google:isEnabled'),
         isGoogleEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-google:isEnabled'),
         isGitHubEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-github:isEnabled'),
         isGitHubEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-github:isEnabled'),
         isTwitterEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-twitter:isEnabled'),
         isTwitterEnabled: await crowi.configManager.getConfig('crowi', 'security:passport-twitter:isEnabled'),
@@ -461,9 +451,6 @@ module.exports = (crowi) => {
         isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:isSameUsernameTreatedAsIdenticalUser'),
         isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:isSameUsernameTreatedAsIdenticalUser'),
         isSameEmailTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:isSameEmailTreatedAsIdenticalUser'),
         isSameEmailTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:isSameEmailTreatedAsIdenticalUser'),
       },
       },
-      basicAuth: {
-        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-basic:isSameUsernameTreatedAsIdenticalUser'),
-      },
       googleOAuth: {
       googleOAuth: {
         googleClientId: await crowi.configManager.getConfig('crowi', 'security:passport-google:clientId'),
         googleClientId: await crowi.configManager.getConfig('crowi', 'security:passport-google:clientId'),
         googleClientSecret: await crowi.configManager.getConfig('crowi', 'security:passport-google:clientSecret'),
         googleClientSecret: await crowi.configManager.getConfig('crowi', 'security:passport-google:clientSecret'),
@@ -562,13 +549,6 @@ module.exports = (crowi) => {
           }
           }
           parameters.action = SupportedAction.ACTION_ADMIN_AUTH_OIDC_DISABLED;
           parameters.action = SupportedAction.ACTION_ADMIN_AUTH_OIDC_DISABLED;
           break;
           break;
-        case 'basic':
-          if (isEnabled) {
-            parameters.action = SupportedAction.ACTION_ADMIN_AUTH_BASIC_ENABLED;
-            break;
-          }
-          parameters.action = SupportedAction.ACTION_ADMIN_AUTH_BASIC_DISABLED;
-          break;
         case 'google':
         case 'google':
           if (isEnabled) {
           if (isEnabled) {
             parameters.action = SupportedAction.ACTION_ADMIN_AUTH_GOOGLE_ENABLED;
             parameters.action = SupportedAction.ACTION_ADMIN_AUTH_GOOGLE_ENABLED;
@@ -1100,49 +1080,6 @@ module.exports = (crowi) => {
     }
     }
   });
   });
 
 
-  /**
-   * @swagger
-   *
-   *    /_api/v3/security-setting/basic:
-   *      put:
-   *        tags: [SecuritySetting, apiv3]
-   *        description: Update basic
-   *        requestBody:
-   *          required: true
-   *          content:
-   *            application/json:
-   *              schema:
-   *                $ref: '#/components/schemas/BasicAuthSetting'
-   *        responses:
-   *          200:
-   *            description: Succeeded to update basic
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  $ref: '#/components/schemas/BasicAuthSetting'
-   */
-  router.put('/basic', loginRequiredStrictly, adminRequired, addActivity, validator.basicAuth, apiV3FormValidator, async(req, res) => {
-    const requestParams = {
-      'security:passport-basic:isSameUsernameTreatedAsIdenticalUser': req.body.isSameUsernameTreatedAsIdenticalUser,
-    };
-
-    try {
-      await updateAndReloadStrategySettings('basic', requestParams);
-
-      const securitySettingParams = {
-        isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-basic:isSameUsernameTreatedAsIdenticalUser'),
-      };
-      const parameters = { action: SupportedAction.ACTION_ADMIN_AUTH_BASIC_UPDATE };
-      activityEvent.emit('update', res.locals.activity._id, parameters);
-      return res.apiv3({ securitySettingParams });
-    }
-    catch (err) {
-      const msg = 'Error occurred in updating basicAuth';
-      logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-basicOAuth-failed'));
-    }
-  });
-
   /**
   /**
    * @swagger
    * @swagger
    *
    *

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

@@ -96,7 +96,6 @@ module.exports = function(crowi, app) {
   app.get('/passport/twitter'                     , loginPassport.loginWithTwitter, loginPassport.loginFailureForExternalAccount);
   app.get('/passport/twitter'                     , loginPassport.loginWithTwitter, loginPassport.loginFailureForExternalAccount);
   app.get('/passport/oidc'                        , loginPassport.loginWithOidc, loginPassport.loginFailureForExternalAccount);
   app.get('/passport/oidc'                        , loginPassport.loginWithOidc, loginPassport.loginFailureForExternalAccount);
   app.get('/passport/saml'                        , loginPassport.loginWithSaml, loginPassport.loginFailureForExternalAccount);
   app.get('/passport/saml'                        , loginPassport.loginWithSaml, loginPassport.loginFailureForExternalAccount);
-  app.get('/passport/basic'                       , loginPassport.loginWithBasic, loginPassport.loginFailureForExternalAccount);
   app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback   , loginPassport.loginFailureForExternalAccount);
   app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback   , loginPassport.loginFailureForExternalAccount);
   app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback   , loginPassport.loginFailureForExternalAccount);
   app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback   , loginPassport.loginFailureForExternalAccount);
   app.get('/passport/twitter/callback'            , loginPassport.loginPassportTwitterCallback  , loginPassport.loginFailureForExternalAccount);
   app.get('/passport/twitter/callback'            , loginPassport.loginPassportTwitterCallback  , loginPassport.loginFailureForExternalAccount);

+ 0 - 44
packages/app/src/server/routes/login-passport.js

@@ -626,49 +626,6 @@ module.exports = function(crowi, app) {
     });
     });
   };
   };
 
 
-  /**
-   * middleware that login with BasicStrategy
-   * @param {*} req
-   * @param {*} res
-   * @param {*} next
-   */
-  const loginWithBasic = async(req, res, next) => {
-    if (!passportService.isBasicStrategySetup) {
-      debug('BasicStrategy has not been set up');
-      const error = new ExternalAccountLoginError('message.strategy_has_not_been_set_up', { strategy: 'Basic' });
-      return next(error);
-    }
-
-    const providerId = 'basic';
-    const strategyName = 'basic';
-    let userId;
-
-    try {
-      userId = await promisifiedPassportAuthentication(strategyName, req, res);
-    }
-    catch (err) {
-      return next(new ExternalAccountLoginError(err.message));
-    }
-
-    const userInfo = {
-      id: userId,
-      username: userId,
-      name: userId,
-    };
-
-    const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
-    if (!externalAccount) {
-      return next(new ExternalAccountLoginError('message.sign_in_failure'));
-    }
-
-    const user = await externalAccount.getPopulatedUser();
-    await req.logIn(user, (err) => {
-      if (err) { debug(err.message); return next(new ExternalAccountLoginError(err.message)) }
-
-      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_BASIC, true);
-    });
-  };
-
   return {
   return {
     cannotLoginErrorHadnler,
     cannotLoginErrorHadnler,
     loginFailure,
     loginFailure,
@@ -681,7 +638,6 @@ module.exports = function(crowi, app) {
     loginWithTwitter,
     loginWithTwitter,
     loginWithOidc,
     loginWithOidc,
     loginWithSaml,
     loginWithSaml,
-    loginWithBasic,
     loginPassportGoogleCallback,
     loginPassportGoogleCallback,
     loginPassportGitHubCallback,
     loginPassportGitHubCallback,
     loginPassportTwitterCallback,
     loginPassportTwitterCallback,

+ 0 - 54
packages/app/src/server/service/passport.ts

@@ -7,7 +7,6 @@ import pRetry from 'p-retry';
 import passport from 'passport';
 import passport from 'passport';
 import { Strategy as GitHubStrategy } from 'passport-github';
 import { Strategy as GitHubStrategy } from 'passport-github';
 import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
 import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
-import { BasicStrategy } from 'passport-http';
 import LdapStrategy from 'passport-ldapauth';
 import LdapStrategy from 'passport-ldapauth';
 import { Strategy as LocalStrategy } from 'passport-local';
 import { Strategy as LocalStrategy } from 'passport-local';
 import { Profile, Strategy as SamlStrategy, VerifiedCallback } from 'passport-saml';
 import { Profile, Strategy as SamlStrategy, VerifiedCallback } from 'passport-saml';
@@ -76,11 +75,6 @@ class PassportService implements S2sMessageHandlable {
    */
    */
   isSamlStrategySetup = false;
   isSamlStrategySetup = false;
 
 
-  /**
-   * the flag whether BasicStrategy is set up successfully
-   */
-  isBasicStrategySetup = false;
-
   /**
   /**
    * the flag whether serializer/deserializer are set up successfully
    * the flag whether serializer/deserializer are set up successfully
    */
    */
@@ -115,10 +109,6 @@ class PassportService implements S2sMessageHandlable {
       setup: 'setupOidcStrategy',
       setup: 'setupOidcStrategy',
       reset: 'resetOidcStrategy',
       reset: 'resetOidcStrategy',
     },
     },
-    basic: {
-      setup: 'setupBasicStrategy',
-      reset: 'resetBasicStrategy',
-    },
     google: {
     google: {
       setup: 'setupGoogleStrategy',
       setup: 'setupGoogleStrategy',
       reset: 'resetGoogleStrategy',
       reset: 'resetGoogleStrategy',
@@ -193,7 +183,6 @@ class PassportService implements S2sMessageHandlable {
     if (this.isLdapStrategySetup) { setupStrategies.push('ldap') }
     if (this.isLdapStrategySetup) { setupStrategies.push('ldap') }
     if (this.isSamlStrategySetup) { setupStrategies.push('saml') }
     if (this.isSamlStrategySetup) { setupStrategies.push('saml') }
     if (this.isOidcStrategySetup) { setupStrategies.push('oidc') }
     if (this.isOidcStrategySetup) { setupStrategies.push('oidc') }
-    if (this.isBasicStrategySetup) { setupStrategies.push('basic') }
     if (this.isGoogleStrategySetup) { setupStrategies.push('google') }
     if (this.isGoogleStrategySetup) { setupStrategies.push('google') }
     if (this.isGitHubStrategySetup) { setupStrategies.push('github') }
     if (this.isGitHubStrategySetup) { setupStrategies.push('github') }
     if (this.isTwitterStrategySetup) { setupStrategies.push('twitter') }
     if (this.isTwitterStrategySetup) { setupStrategies.push('twitter') }
@@ -991,49 +980,6 @@ class PassportService implements S2sMessageHandlable {
     return result;
     return result;
   }
   }
 
 
-  /**
-   * reset BasicStrategy
-   *
-   * @memberof PassportService
-   */
-  resetBasicStrategy() {
-    logger.debug('BasicStrategy: reset');
-    passport.unuse('basic');
-    this.isBasicStrategySetup = false;
-  }
-
-  /**
-   * setup BasicStrategy
-   *
-   * @memberof PassportService
-   */
-  setupBasicStrategy() {
-
-    this.resetBasicStrategy();
-
-    const configManager = this.crowi.configManager;
-    const isBasicEnabled = configManager.getConfig('crowi', 'security:passport-basic:isEnabled');
-
-    // when disabled
-    if (!isBasicEnabled) {
-      return;
-    }
-
-    logger.debug('BasicStrategy: setting up..');
-
-    passport.use(new BasicStrategy(
-      (userId, password, done) => {
-        if (userId != null) {
-          return done(null, userId);
-        }
-        return done(null, false, { message: 'Incorrect credentials.' });
-      },
-    ));
-
-    this.isBasicStrategySetup = true;
-    logger.debug('BasicStrategy: setup is done');
-  }
-
   /**
   /**
    * setup serializer and deserializer
    * setup serializer and deserializer
    *
    *

+ 7 - 12
packages/app/test/cypress/integration/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.spec.ts

@@ -3,16 +3,14 @@ context('Access to page by guest', () => {
 
 
   it('/Sandbox is successfully loaded', () => {
   it('/Sandbox is successfully loaded', () => {
     cy.visit('/Sandbox');
     cy.visit('/Sandbox');
-    cy.waitUntilSpinnerDisappear();
     cy.getByTestid('grw-pagetree-item-container').should('be.visible');
     cy.getByTestid('grw-pagetree-item-container').should('be.visible');
-    cy.collapseSidebar(true, true);
+    cy.collapseSidebar(true);
     cy.screenshot(`${ssPrefix}-sandbox`);
     cy.screenshot(`${ssPrefix}-sandbox`);
   });
   });
 
 
   it('/Sandbox with anchor hash is successfully loaded', () => {
   it('/Sandbox with anchor hash is successfully loaded', () => {
     cy.visit('/Sandbox#Headers');
     cy.visit('/Sandbox#Headers');
     cy.getByTestid('grw-pagetree-item-container').should('be.visible');
     cy.getByTestid('grw-pagetree-item-container').should('be.visible');
-    cy.collapseSidebar(true, true);
 
 
     // eslint-disable-next-line cypress/no-unnecessary-waiting
     // eslint-disable-next-line cypress/no-unnecessary-waiting
     // cy.wait(500);
     // cy.wait(500);
@@ -20,16 +18,17 @@ context('Access to page by guest', () => {
     // hide fab // disable fab for sticky-events warning
     // hide fab // disable fab for sticky-events warning
     // cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
     // cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
 
 
+    cy.collapseSidebar(true, true);
     cy.screenshot(`${ssPrefix}-sandbox-headers`);
     cy.screenshot(`${ssPrefix}-sandbox-headers`);
   });
   });
 
 
   it('/Sandbox/Math is successfully loaded', () => {
   it('/Sandbox/Math is successfully loaded', () => {
     cy.visit('/Sandbox/Math');
     cy.visit('/Sandbox/Math');
     cy.getByTestid('revision-toc-content').should('be.visible');
     cy.getByTestid('revision-toc-content').should('be.visible');
-    cy.collapseSidebar(true, true);
 
 
     cy.get('.math').should('be.visible');
     cy.get('.math').should('be.visible');
 
 
+    cy.collapseSidebar(true);
     cy.screenshot(`${ssPrefix}-sandbox-math`, {
     cy.screenshot(`${ssPrefix}-sandbox-math`, {
       blackout: ['.revision-toc', '[data-hide-in-vrt=true]']
       blackout: ['.revision-toc', '[data-hide-in-vrt=true]']
     });
     });
@@ -37,11 +36,11 @@ context('Access to page by guest', () => {
 
 
   it('/Sandbox with edit is successfully loaded', () => {
   it('/Sandbox with edit is successfully loaded', () => {
     cy.visit('/Sandbox#edit');
     cy.visit('/Sandbox#edit');
-    cy.collapseSidebar(true, true);
 
 
     // eslint-disable-next-line cypress/no-unnecessary-waiting
     // eslint-disable-next-line cypress/no-unnecessary-waiting
     cy.wait(1000);
     cy.wait(1000);
 
 
+    cy.collapseSidebar(true);
     cy.screenshot(`${ssPrefix}-sandbox-edit-page`);
     cy.screenshot(`${ssPrefix}-sandbox-edit-page`);
   })
   })
 
 
@@ -51,13 +50,9 @@ context('Access to page by guest', () => {
 context('Access to /me page', () => {
 context('Access to /me page', () => {
   const ssPrefix = 'access-to-me-page-by-guest-';
   const ssPrefix = 'access-to-me-page-by-guest-';
 
 
-  beforeEach(() => {
-    // collapse sidebar
-    cy.collapseSidebar(true);
-  });
-
   it('/me should be redirected to /login', () => {
   it('/me should be redirected to /login', () => {
-    cy.visit('/me', {  });
+    cy.visit('/me');
+    cy.getByTestid('login-form').should('be.visible');
     cy.screenshot(`${ssPrefix}-me`);
     cy.screenshot(`${ssPrefix}-me`);
   });
   });
 
 
@@ -69,8 +64,8 @@ context('Access to special pages by guest', () => {
 
 
   it('/trash is successfully loaded', () => {
   it('/trash is successfully loaded', () => {
     cy.visit('/trash', {  });
     cy.visit('/trash', {  });
-    cy.collapseSidebar(true, true);
     cy.getByTestid('trash-page-list').should('be.visible');
     cy.getByTestid('trash-page-list').should('be.visible');
+    cy.collapseSidebar(true);
     cy.screenshot(`${ssPrefix}-trash`);
     cy.screenshot(`${ssPrefix}-trash`);
   });
   });
 
 

+ 14 - 12
packages/app/test/cypress/integration/60-home/60-home--home.spec.ts

@@ -6,27 +6,30 @@ context('Access Home', () => {
     cy.fixture("user-admin.json").then(user => {
     cy.fixture("user-admin.json").then(user => {
       cy.login(user.username, user.password);
       cy.login(user.username, user.password);
     });
     });
-    // collapse sidebar
-    cy.collapseSidebar(true);
   });
   });
 
 
   it('Visit home', () => {
   it('Visit home', () => {
     cy.visit('/dummy');
     cy.visit('/dummy');
-    cy.waitUntilSkeletonDisappear();
-    cy.get('.grw-personal-dropdown').as('dropdown').should('be.visible').click()
-    cy.get('@dropdown').within(()=>{
-      cy.getByTestid('personal-dropdown-menu').should('have.css', 'display', 'block');
+
+    // open PersonalDropdown
+    cy.waitUntil(() => {
+      // do
+      cy.getByTestid('personal-dropdown-button').should('be.visible').click();
+      // wait until
+      return cy.getByTestid('grw-personal-dropdown-menu-user-home').then($elem => $elem.is(':visible'));
     });
     });
+    // click the Home button
     cy.getByTestid('grw-personal-dropdown-menu-user-home').should('be.visible').click();
     cy.getByTestid('grw-personal-dropdown-menu-user-home').should('be.visible').click();
-    cy.waitUntilSkeletonDisappear();
 
 
-    // eslint-disable-next-line cypress/no-unnecessary-waiting
-    cy.wait(2000); // wait for calcViewHeight and rendering
+    cy.getByTestid('grw-users-info').should('be.visible');
 
 
     // for check download toc data
     // for check download toc data
-    cy.get('.toc-link', { timeout: 60000 }).should('be.visible');
+    // https://redmine.weseek.co.jp/issues/111384
+    // cy.get('.toc-link').should('be.visible');
 
 
     // same screenshot is taken in access-to-page.spec
     // same screenshot is taken in access-to-page.spec
+    cy.collapseSidebar(true);
+    cy.waitUntilSkeletonDisappear();
     cy.screenshot(`${ssPrefix}-visit-home`);
     cy.screenshot(`${ssPrefix}-visit-home`);
   });
   });
 
 
@@ -41,9 +44,8 @@ context('Access User settings', () => {
     cy.fixture("user-admin.json").then(user => {
     cy.fixture("user-admin.json").then(user => {
       cy.login(user.username, user.password);
       cy.login(user.username, user.password);
     });
     });
-    // collapse sidebar
-    cy.collapseSidebar(true);
     cy.visit('/me');
     cy.visit('/me');
+    cy.collapseSidebar(true);
     // hide fab // disable fab for sticky-events warning
     // hide fab // disable fab for sticky-events warning
     // cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
     // cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
   });
   });

+ 34 - 23
packages/app/test/cypress/support/commands.ts

@@ -24,6 +24,20 @@
 // -- This will overwrite an existing command --
 // -- This will overwrite an existing command --
 // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
 // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
 
 
+import 'cypress-wait-until';
+
+function isVisible($elem: JQuery<Element>) {
+  return $elem.is(':visible');
+}
+function isHidden($elem: JQuery<Element>) {
+  return !isVisible($elem);
+}
+function isVisibleByTestId(testId: string) {
+  return isVisible(Cypress.$(`[data-testid=${testId}]`));
+}
+function isHiddenByTestId(testId: string) {
+  return !isVisibleByTestId(testId);
+}
 
 
 Cypress.Commands.add('getByTestid', (selector, options?) => {
 Cypress.Commands.add('getByTestid', (selector, options?) => {
   return cy.get(`[data-testid=${selector}]`, options);
   return cy.get(`[data-testid=${selector}]`, options);
@@ -41,43 +55,40 @@ Cypress.Commands.add('login', (username, password) => {
   });
   });
 });
 });
 
 
-/**
- * use only for the pages which use component with skeleton
- */
 Cypress.Commands.add('waitUntilSkeletonDisappear', () => {
 Cypress.Commands.add('waitUntilSkeletonDisappear', () => {
-  cy.get('.grw-skeleton').should('exist');
+  if (isHidden(Cypress.$('.grw-skeleton'))) {
+    return;
+  }
   cy.get('.grw-skeleton').should('not.exist');
   cy.get('.grw-skeleton').should('not.exist');
 });
 });
 
 
 Cypress.Commands.add('waitUntilSpinnerDisappear', () => {
 Cypress.Commands.add('waitUntilSpinnerDisappear', () => {
-  cy.get('.fa-spinner').should('exist');
+  if (isHidden(Cypress.$('.fa-spinner'))) {
+    return;
+  }
   cy.get('.fa-spinner').should('not.exist');
   cy.get('.fa-spinner').should('not.exist');
 });
 });
 
 
-let isSidebarCollapsed: boolean | undefined;
-
-Cypress.Commands.add('collapseSidebar', (isCollapsed, force=false) => {
+Cypress.Commands.add('collapseSidebar', (isCollapsed: boolean) => {
+  const isSidebarExists = isVisibleByTestId('grw-sidebar-wrapper');
 
 
-  if (!force && isSidebarCollapsed === isCollapsed) {
+  if (!isSidebarExists) {
     return;
     return;
   }
   }
 
 
-  const isGrowiPage = Cypress.$('div.growi').length > 0;
-  if (!isGrowiPage) {
-    cy.visit('/page-to-toggle-sidebar-collapsed');
+  const isSidebarContextualNavigationHidden = isHiddenByTestId('grw-contextual-navigation-sub');
+  if (isSidebarContextualNavigationHidden === isCollapsed) {
+    return;
   }
   }
 
 
-  cy.getByTestid('grw-contextual-navigation-sub').then(($contents) => {
-    const isCurrentCollapsed = $contents.hasClass('d-none');
-    // toggle when the current state and isCoolapsed is not match
-    if (isCurrentCollapsed !== isCollapsed) {
-      cy.getByTestid("grw-navigation-resize-button").click({force: true});
+  cy.waitUntil(() => {
+    // do
+    cy.getByTestid("grw-navigation-resize-button").click({force: true});
+    // wait until saving UserUISettings
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(1500);
 
 
-      // wait until saving UserUISettings
-      // eslint-disable-next-line cypress/no-unnecessary-waiting
-      cy.wait(1500);
-    }
+    // wait until
+    return cy.getByTestid('grw-contextual-navigation-sub').then($contents => isHidden($contents) === isCollapsed);
   });
   });
-
-  isSidebarCollapsed = isCollapsed;
 });
 });

+ 5 - 7
yarn.lock

@@ -7800,6 +7800,11 @@ currently-unhandled@^0.4.1:
   dependencies:
   dependencies:
     array-find-index "^1.0.1"
     array-find-index "^1.0.1"
 
 
+cypress-wait-until@^1.7.2:
+  version "1.7.2"
+  resolved "https://registry.yarnpkg.com/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz#7f534dd5a11c89b65359e7a0210f20d3dfc22107"
+  integrity sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q==
+
 cypress@^12.0.1:
 cypress@^12.0.1:
   version "12.0.1"
   version "12.0.1"
   resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.0.1.tgz#3a51a38b2f162256c7226e68e902cfe1750e3d92"
   resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.0.1.tgz#3a51a38b2f162256c7226e68e902cfe1750e3d92"
@@ -17563,13 +17568,6 @@ passport-google-oauth20@^2.0.0:
   dependencies:
   dependencies:
     passport-oauth2 "1.x.x"
     passport-oauth2 "1.x.x"
 
 
-passport-http@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/passport-http/-/passport-http-0.3.0.tgz#8ee53d4380be9c60df2151925029826f77115603"
-  integrity sha1-juU9Q4C+nGDfIVGSUCmCb3cRVgM=
-  dependencies:
-    passport-strategy "1.x.x"
-
 passport-ldapauth@^3.0.1:
 passport-ldapauth@^3.0.1:
   version "3.0.1"
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/passport-ldapauth/-/passport-ldapauth-3.0.1.tgz#1432e8469de18bd46b5b39a46a866b416c1ddded"
   resolved "https://registry.yarnpkg.com/passport-ldapauth/-/passport-ldapauth-3.0.1.tgz#1432e8469de18bd46b5b39a46a866b416c1ddded"