Browse Source

Merge branch 'support/apply-nextjs-2' into imprv/101156-delete-unused-code-related-to-theme-preview-and-CustomBarEditor

kaori 3 years ago
parent
commit
6f6abe56e2
46 changed files with 920 additions and 392 deletions
  1. 2 2
      .github/workflows/reusable-app-prod.yml
  2. 4 2
      packages/app/package.json
  3. 1 7
      packages/app/src/client/services/AdminBasicSecurityContainer.js
  4. 1 6
      packages/app/src/client/services/AdminGeneralSecurityContainer.js
  5. 0 1
      packages/app/src/client/services/AdminGitHubSecurityContainer.js
  6. 0 1
      packages/app/src/client/services/AdminGoogleSecurityContainer.js
  7. 1 4
      packages/app/src/client/services/AdminLdapSecurityContainer.js
  8. 1 5
      packages/app/src/client/services/AdminOidcSecurityContainer.js
  9. 1 5
      packages/app/src/client/services/AdminSamlSecurityContainer.js
  10. 1 5
      packages/app/src/client/services/AdminTwitterSecurityContainer.js
  11. 18 27
      packages/app/src/components/Admin/Security/BasicSecuritySetting.jsx
  12. 2 5
      packages/app/src/components/Admin/Security/FacebookSecuritySetting.jsx
  13. 17 22
      packages/app/src/components/Admin/Security/GitHubSecuritySetting.jsx
  14. 13 6
      packages/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx
  15. 18 22
      packages/app/src/components/Admin/Security/GoogleSecuritySetting.jsx
  16. 14 8
      packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  17. 17 21
      packages/app/src/components/Admin/Security/LdapSecuritySetting.jsx
  18. 18 22
      packages/app/src/components/Admin/Security/LocalSecuritySetting.jsx
  19. 9 12
      packages/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx
  20. 17 22
      packages/app/src/components/Admin/Security/OidcSecuritySetting.jsx
  21. 19 13
      packages/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx
  22. 17 22
      packages/app/src/components/Admin/Security/SamlSecuritySetting.jsx
  23. 15 5
      packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  24. 14 21
      packages/app/src/components/Admin/Security/SecurityManagement.jsx
  25. 18 18
      packages/app/src/components/Admin/Security/SecurityManagementContents.jsx
  26. 17 25
      packages/app/src/components/Admin/Security/TwitterSecuritySetting.jsx
  27. 13 5
      packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx
  28. 10 9
      packages/app/src/components/LoginForm.jsx
  29. 3 5
      packages/app/src/components/Page/DisplaySwitcher.tsx
  30. 2 1
      packages/app/src/components/UnstatedUtils.tsx
  31. 5 4
      packages/app/src/migrations/20211129125654-initialize-private-legacy-pages-named-query.js
  32. 22 14
      packages/app/src/pages/[[...path]].page.tsx
  33. 3 0
      packages/app/src/pages/_app.page.tsx
  34. 166 6
      packages/app/src/pages/_search.page.tsx
  35. 13 1
      packages/app/src/pages/admin/[[...path]].page.tsx
  36. 20 9
      packages/app/src/pages/installer.page.tsx
  37. 112 0
      packages/app/src/pages/login.page.tsx
  38. 133 4
      packages/app/src/server/models/page-redirect.ts
  39. 1 1
      packages/app/src/server/routes/index.js
  40. 12 12
      packages/app/src/server/routes/login.js
  41. 1 1
      packages/app/src/server/routes/page.js
  42. 4 0
      packages/app/src/stores/context.tsx
  43. 13 10
      packages/app/src/styles/_login.scss
  44. 1 1
      packages/app/src/styles/style-next.scss
  45. 111 0
      packages/app/test/integration/models/page-redirect.test.js
  46. 20 0
      yarn.lock

+ 2 - 2
.github/workflows/reusable-app-prod.yml

@@ -60,7 +60,7 @@ jobs:
     - name: Archive production files
       id: archive-prod-files
       run: |
-        tar -cf production.tar \
+        tar -zcf production.tar.gz \
           package.json \
           packages/app/.next \
           packages/app/config \
@@ -70,7 +70,7 @@ jobs:
           packages/app/.env.production* \
           packages/*/package.json \
           packages/*/dist
-        echo ::set-output name=file::production.tar
+        echo ::set-output name=file::production.tar.gz
 
     - name: Upload production files as artifact
       uses: actions/upload-artifact@v3

+ 4 - 2
packages/app/package.json

@@ -21,7 +21,7 @@
     "predev": "yarn cross-env NODE_ENV=development run-p resources:* dev:migrate:up",
     "dev:analyze": "yarn cross-env ANALYZE=true yarn dev",
     "dev:migrate-mongo": "yarn cross-env NODE_ENV=development yarn ts-node node_modules/.bin/migrate-mongo",
-    "dev:migrate": "yarn dev:migrate:up -f config/migrate-mongo-config.js",
+    "dev:migrate": "yarn dev:migrate:up",
     "dev:migrate:create": "yarn dev:migrate-mongo create -f config/migrate-mongo-config.js",
     "dev:migrate:status": "yarn dev:migrate-mongo status -f config/migrate-mongo-config.js",
     "dev:migrate:up": "yarn dev:migrate-mongo up -f config/migrate-mongo-config.js",
@@ -179,7 +179,8 @@
   },
   "// comments for defDependencies": {
     "@handsontable/react": "v3 requires handsontable >= 7.0.0.",
-    "handsontable": "v7.0.0 or above is no loger MIT lisence."
+    "handsontable": "v7.0.0 or above is no loger MIT lisence.",
+    "ts-node": "v10 occurs 'SyntaxError: Cannot use import statement outside a module' when using migrate-mongo"
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
@@ -243,6 +244,7 @@
     "swr": "^1.3.0",
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
+    "ts-node": "^9.1.1",
     "ts-node-dev": "^2.0.0",
     "tsc-alias": "^1.2.9",
     "unstated": "^2.1.1",

+ 1 - 7
packages/app/src/client/services/AdminBasicSecurityContainer.js

@@ -16,15 +16,9 @@ export default class AdminBasicSecurityContainer extends Container {
   constructor() {
     super();
 
-    this.dummyIsSameUsernameTreatedAsIdenticalUser = 0;
-    this.dummyIsSameUsernameTreatedAsIdenticalUserForError = 1;
-
     this.state = {
-      retrieveError: null,
-      // set dummy value tile for using suspense
-      isSameUsernameTreatedAsIdenticalUser: this.dummyIsSameUsernameTreatedAsIdenticalUser,
+      isSameUsernameTreatedAsIdenticalUser: false,
     };
-
   }
 
   /**

+ 1 - 6
packages/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -18,15 +18,11 @@ export default class AdminGeneralSecurityContainer extends Container {
   constructor(appContainer) {
     super();
 
-    this.dummyCurrentRestrictGuestMode = 0;
-    this.dummyCurrentRestrictGuestModeForError = 1;
-
     this.state = {
       retrieveError: null,
       sessionMaxAge: null,
       wikiMode: '',
-      // set dummy value tile for using suspense
-      currentRestrictGuestMode: this.dummyCurrentRestrictGuestMode,
+      currentRestrictGuestMode: '',
       currentPageDeletionAuthority: PageSingleDeleteConfigValue.AdminOnly,
       currentPageRecursiveDeletionAuthority: PageRecursiveDeleteConfigValue.Inherit,
       currentPageCompleteDeletionAuthority: PageSingleDeleteCompConfigValue.AdminOnly,
@@ -37,7 +33,6 @@ export default class AdminGeneralSecurityContainer extends Container {
       expandOtherOptionsForCompleteDeletion: false,
       isShowRestrictedByOwner: false,
       isShowRestrictedByGroup: false,
-      appSiteUrl: appContainer.config.crowi.url || '',
       isLocalEnabled: false,
       isLdapEnabled: false,
       isSamlEnabled: false,

+ 0 - 1
packages/app/src/client/services/AdminGitHubSecurityContainer.js

@@ -23,7 +23,6 @@ export default class AdminGitHubSecurityContainer extends Container {
 
     this.state = {
       retrieveError: null,
-      callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/github/callback'),
       // set dummy value tile for using suspense
       githubClientId: this.dummyGithubClientId,
       githubClientSecret: '',

+ 0 - 1
packages/app/src/client/services/AdminGoogleSecurityContainer.js

@@ -23,7 +23,6 @@ export default class AdminGoogleSecurityContainer extends Container {
 
     this.state = {
       retrieveError: null,
-      callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/google/callback'),
       // set dummy value tile for using suspense
       googleClientId: this.dummyGoogleClientId,
       googleClientSecret: '',

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

@@ -17,13 +17,10 @@ export default class AdminLdapSecurityContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummyServerUrl = 0;
-    this.dummyServerUrlForError = 1;
 
     this.state = {
       retrieveError: null,
-      // set dummy value tile for using suspense
-      serverUrl: this.dummyServerUrl,
+      serverUrl: '',
       isUserBind: false,
       ldapBindDN: '',
       ldapBindDNPassword: '',

+ 1 - 5
packages/app/src/client/services/AdminOidcSecurityContainer.js

@@ -19,14 +19,10 @@ export default class AdminOidcSecurityContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummyOidcProviderName = 0;
-    this.dummyOidcProviderNameForError = 1;
 
     this.state = {
       retrieveError: null,
-      callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/oidc/callback'),
-      // set dummy value tile for using suspense
-      oidcProviderName: this.dummyOidcProviderName,
+      oidcProviderName: '',
       oidcIssuerHost: '',
       oidcAuthorizationEndpoint: '',
       oidcTokenEndpoint: '',

+ 1 - 5
packages/app/src/client/services/AdminSamlSecurityContainer.js

@@ -19,17 +19,13 @@ export default class AdminSamlSecurityContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummySamlEntryPoint = 0;
-    this.dummySamlEntryPointForError = 1;
 
     this.state = {
       retrieveError: null,
       // TODO GW-1324 ABLCRure DB value takes precedence
       useOnlyEnvVars: false,
-      callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/saml/callback'),
       missingMandatoryConfigKeys: [],
-      // set dummy value tile for using suspense
-      samlEntryPoint: this.dummySamlEntryPoint,
+      samlEntryPoint: '',
       samlIssuer: '',
       samlCert: '',
       samlAttrMapId: '',

+ 1 - 5
packages/app/src/client/services/AdminTwitterSecurityContainer.js

@@ -19,13 +19,9 @@ export default class AdminTwitterSecurityContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummyTwitterConsumerKey = 0;
-    this.dummyTwitterConsumerKeyForError = 1;
 
     this.state = {
-      callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/twitter/callback'),
-      // set dummy value tile for using suspense
-      twitterConsumerKey: this.dummyTwitterConsumerKey,
+      twitterConsumerKey: '',
       twitterConsumerSecret: '',
       isSameUsernameTreatedAsIdenticalUser: false,
     };

+ 18 - 27
packages/app/src/components/Admin/Security/BasicSecuritySetting.jsx

@@ -1,5 +1,4 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
+import React, { useEffect, useCallback } from 'react';
 
 import PropTypes from 'prop-types';
 
@@ -11,34 +10,26 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import BasicSecurityManagementContents from './BasicSecuritySettingContents';
 
-let retrieveErrors = null;
-function BasicSecurityManagement(props) {
+const BasicSecurityManagement = (props) => {
   const { adminBasicSecurityContainer } = props;
-  if (adminBasicSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser === adminBasicSecurityContainer.dummyIsSameUsernameTreatedAsIdenticalUser) {
-    throw (async() => {
-      try {
-        await adminBasicSecurityContainer.retrieveSecurityData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        retrieveErrors = errs;
-        adminBasicSecurityContainer.setState({
-          isSameUsernameTreatedAsIdenticalUser: adminBasicSecurityContainer.dummyIsSameUsernameTreatedAsIdenticalUser,
-        });
-
-      }
-    })();
-  }
-
-  if (
-    adminBasicSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser === adminBasicSecurityContainer.dummyIsSameUsernameTreatedAsIdenticalUserForError
-  ) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  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,

+ 2 - 5
packages/app/src/components/Admin/Security/FacebookSecuritySetting.jsx

@@ -1,4 +1,3 @@
-/* eslint-disable react/no-danger */
 import React from 'react';
 
 import { withTranslation } from 'next-i18next';
@@ -14,15 +13,13 @@ class FacebookSecurityManagement extends React.Component {
   render() {
     const { t } = this.props;
     return (
-      <React.Fragment>
-
+      <>
         <h2 className="alert-anchor border-bottom">
           Facebook OAuth { t('security_setting.configuration') }
         </h2>
 
         <p className="well">(TBD)</p>
-
-      </React.Fragment>
+      </>
     );
   }
 

+ 17 - 22
packages/app/src/components/Admin/Security/GitHubSecuritySetting.jsx

@@ -1,5 +1,4 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
+import React, { useEffect, useCallback } from 'react';
 
 import PropTypes from 'prop-types';
 
@@ -12,29 +11,25 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import GitHubSecuritySettingContents from './GitHubSecuritySettingContents';
 
-let retrieveErrors = null;
-function GitHubSecurityManagement(props) {
+const GitHubSecurityManagement = (props) => {
   const { adminGitHubSecurityContainer } = props;
-  if (adminGitHubSecurityContainer.state.githubClientId === adminGitHubSecurityContainer.dummyGithubClientId) {
-    throw (async() => {
-      try {
-        await adminGitHubSecurityContainer.retrieveSecurityData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        retrieveErrors = errs;
-        adminGitHubSecurityContainer.setState({ githubClientId: adminGitHubSecurityContainer.dummyGithubClientIdForError });
-      }
-    })();
-  }
-
-  if (adminGitHubSecurityContainer.state.githubClientId === adminGitHubSecurityContainer.dummyGithubClientIdForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  const fetchGitHubSecuritySettingsData = useCallback(async() => {
+    try {
+      await adminGitHubSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+    }
+  }, [adminGitHubSecurityContainer]);
+
+  useEffect(() => {
+    fetchGitHubSecuritySettingsData();
+  }, [adminGitHubSecurityContainer, fetchGitHubSecuritySettingsData]);
 
   return <GitHubSecuritySettingContents />;
-}
+};
 
 
 GitHubSecurityManagement.propTypes = {

+ 13 - 6
packages/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx

@@ -1,13 +1,16 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
 
-import PropTypes from 'prop-types';
+import { pathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
+import urljoin from 'url-join';
 
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { useSiteUrl } from '~/stores/context';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -33,8 +36,11 @@ class GitHubSecurityManagementContents extends React.Component {
   }
 
   render() {
-    const { t, adminGeneralSecurityContainer, adminGitHubSecurityContainer } = this.props;
+    const {
+      t, adminGeneralSecurityContainer, adminGitHubSecurityContainer, siteUrl,
+    } = this.props;
     const { isGitHubEnabled } = adminGeneralSecurityContainer.state;
+    const gitHubCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/github/callback');
 
     return (
 
@@ -75,11 +81,11 @@ class GitHubSecurityManagementContents extends React.Component {
             <input
               className="form-control"
               type="text"
-              value={adminGitHubSecurityContainer.state.appSiteUrl}
+              value={gitHubCallbackUrl}
               readOnly
             />
             <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
-            {!adminGeneralSecurityContainer.state.appSiteUrl && (
+            {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
@@ -172,7 +178,7 @@ class GitHubSecurityManagementContents extends React.Component {
           <ol id="collapseHelpForGitHubOauth" className="collapse">
             {/* eslint-disable-next-line max-len */}
             <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.GitHub.register_1', { link: '<a href="https://github.com/settings/developers" target=_blank>GitHub Developer Settings</a>' }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.GitHub.register_2', { url: adminGitHubSecurityContainer.state.callbackUrl }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.GitHub.register_2', { url: gitHubCallbackUrl }) }} />
             <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.GitHub.register_3') }} />
           </ol>
         </div>
@@ -187,7 +193,8 @@ class GitHubSecurityManagementContents extends React.Component {
 
 const GitHubSecurityManagementContentsFC = (props) => {
   const { t } = useTranslation();
-  return <GitHubSecurityManagementContents t={t} {...props} />;
+  const { data: siteUrl } = useSiteUrl();
+  return <GitHubSecurityManagementContents t={t} siteUrl={siteUrl} {...props} />;
 };
 
 /**

+ 18 - 22
packages/app/src/components/Admin/Security/GoogleSecuritySetting.jsx

@@ -1,5 +1,4 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
+import React, { useEffect, useCallback } from 'react';
 
 import PropTypes from 'prop-types';
 
@@ -11,29 +10,26 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import GoogleSecurityManagementContents from './GoogleSecuritySettingContents';
 
-let retrieveErrors = null;
-function GoogleSecurityManagement(props) {
+const GoogleSecurityManagement = (props) => {
   const { adminGoogleSecurityContainer } = props;
-  if (adminGoogleSecurityContainer.state.googleClientId === adminGoogleSecurityContainer.dummyGoogleClientId) {
-    throw (async() => {
-      try {
-        await adminGoogleSecurityContainer.retrieveSecurityData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        retrieveErrors = errs;
-        adminGoogleSecurityContainer.setState({ googleClientId: adminGoogleSecurityContainer.dummyGoogleClientIdForError });
-      }
-    })();
-  }
-
-  if (adminGoogleSecurityContainer.state.googleClientId === adminGoogleSecurityContainer.dummyGoogleClientIdForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  const fetchGoogleSecuritySettingsData = useCallback(async() => {
+    try {
+      await adminGoogleSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+    }
+  }, [adminGoogleSecurityContainer]);
+
+
+  useEffect(() => {
+    fetchGoogleSecuritySettingsData();
+  }, [adminGoogleSecurityContainer, fetchGoogleSecuritySettingsData]);
 
   return <GoogleSecurityManagementContents />;
-}
+};
 
 
 GoogleSecurityManagement.propTypes = {

+ 14 - 8
packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx

@@ -1,13 +1,14 @@
-/* eslint-disable react/no-danger */
 import React from 'react';
 
-import PropTypes from 'prop-types';
+import { pathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
-
+import PropTypes from 'prop-types';
+import urljoin from 'url-join';
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { useSiteUrl } from '~/stores/context';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -33,8 +34,11 @@ class GoogleSecurityManagementContents extends React.Component {
   }
 
   render() {
-    const { t, adminGeneralSecurityContainer, adminGoogleSecurityContainer } = this.props;
+    const {
+      t, adminGeneralSecurityContainer, adminGoogleSecurityContainer, siteUrl,
+    } = this.props;
     const { isGoogleEnabled } = adminGeneralSecurityContainer.state;
+    const googleCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/google/callback');
 
     return (
 
@@ -75,11 +79,11 @@ class GoogleSecurityManagementContents extends React.Component {
             <input
               className="form-control"
               type="text"
-              value={adminGoogleSecurityContainer.state.callbackUrl}
+              value={googleCallbackUrl}
               readOnly
             />
             <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
-            {!adminGeneralSecurityContainer.state.appSiteUrl && (
+            {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
@@ -179,7 +183,7 @@ class GoogleSecurityManagementContents extends React.Component {
             <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_1', { link: '<a href="https://console.cloud.google.com/apis/credentials" target=_blank>Google Cloud Platform API Manager</a>' }) }} />
             <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_2') }} />
             <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_3') }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_4', { url: adminGoogleSecurityContainer.state.callbackUrl }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_4', { url: googleCallbackUrl }) }} />
             <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_5') }} />
           </ol>
         </div>
@@ -194,7 +198,8 @@ class GoogleSecurityManagementContents extends React.Component {
 
 const GoogleSecurityManagementContentsFc = (props) => {
   const { t } = useTranslation();
-  return <GoogleSecurityManagementContents t={t} {...props} />;
+  const { data: siteUrl } = useSiteUrl();
+  return <GoogleSecurityManagementContents t={t} siteUrl={siteUrl} {...props} />;
 };
 
 
@@ -202,6 +207,7 @@ GoogleSecurityManagementContents.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
   adminGoogleSecurityContainer: PropTypes.instanceOf(AdminGoogleSecurityContainer).isRequired,
+  siteUrl: PropTypes.string,
 };
 
 const GoogleSecurityManagementContentsWrapper = withUnstatedContainers(GoogleSecurityManagementContentsFc, [

+ 17 - 21
packages/app/src/components/Admin/Security/LdapSecuritySetting.jsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect, useCallback } from 'react';
 
 import PropTypes from 'prop-types';
 
@@ -10,29 +10,25 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import LdapSecuritySettingContents from './LdapSecuritySettingContents';
 
-let retrieveErrors = null;
-function LdapSecuritySetting(props) {
+const LdapSecuritySetting = (props) => {
   const { adminLdapSecurityContainer } = props;
-  if (adminLdapSecurityContainer.state.serverUrl === adminLdapSecurityContainer.dummyServerUrl) {
-    throw (async() => {
-      try {
-        await adminLdapSecurityContainer.retrieveSecurityData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        retrieveErrors = errs;
-        adminLdapSecurityContainer.setState({ serverUrl: adminLdapSecurityContainer.dummyServerUrlForError });
-      }
-    })();
-  }
-
-  if (adminLdapSecurityContainer.state.serverUrl === adminLdapSecurityContainer.dummyServerUrlForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  const fetchLdapSecuritySettingsData = useCallback(async() => {
+    try {
+      await adminLdapSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+    }
+  }, [adminLdapSecurityContainer]);
+
+  useEffect(() => {
+    fetchLdapSecuritySettingsData();
+  }, [adminLdapSecurityContainer, fetchLdapSecuritySettingsData]);
 
   return <LdapSecuritySettingContents />;
-}
+};
 
 LdapSecuritySetting.propTypes = {
   adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,

+ 18 - 22
packages/app/src/components/Admin/Security/LocalSecuritySetting.jsx

@@ -1,5 +1,4 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
+import React, { useEffect, useCallback } from 'react';
 
 import PropTypes from 'prop-types';
 
@@ -11,29 +10,26 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import LocalSecuritySettingContents from './LocalSecuritySettingContents';
 
-let retrieveErrors = null;
-function LocalSecuritySetting(props) {
+const LocalSecuritySetting = (props) => {
   const { adminLocalSecurityContainer } = props;
-  if (adminLocalSecurityContainer.state.registrationMode === adminLocalSecurityContainer.dummyRegistrationMode) {
-    throw (async() => {
-      try {
-        await adminLocalSecurityContainer.retrieveSecurityData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        retrieveErrors = errs;
-        adminLocalSecurityContainer.setState({ registrationMode: adminLocalSecurityContainer.dummyRegistrationModeForError });
-      }
-    })();
-  }
-
-  if (adminLocalSecurityContainer.state.registrationMode === adminLocalSecurityContainer.dummyRegistrationModeForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  const fetchLocalSecuritySettingsData = useCallback(async() => {
+    try {
+      await adminLocalSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+    }
+  }, [adminLocalSecurityContainer]);
+
+
+  useEffect(() => {
+    fetchLocalSecuritySettingsData();
+  }, [adminLocalSecurityContainer, fetchLocalSecuritySettingsData]);
 
   return <LocalSecuritySettingContents />;
-}
+};
 
 LocalSecuritySetting.propTypes = {
   adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer).isRequired,

+ 9 - 12
packages/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx

@@ -1,14 +1,13 @@
-/* eslint-disable react/no-danger */
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { useIsMailerSetup } from '~/stores/context';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -37,14 +36,13 @@ class LocalSecuritySettingContents extends React.Component {
       t,
       adminGeneralSecurityContainer,
       adminLocalSecurityContainer,
-      appContainer,
+      isMailerSetup,
     } = this.props;
     const { registrationMode, isPasswordResetEnabled, isEmailAuthenticationEnabled } = adminLocalSecurityContainer.state;
     const { isLocalEnabled } = adminGeneralSecurityContainer.state;
-    const { isMailerSetup } = appContainer.config;
 
     return (
-      <React.Fragment>
+      <>
         {adminLocalSecurityContainer.state.retrieveError != null && (
           <div className="alert alert-danger">
             <p>
@@ -97,7 +95,7 @@ class LocalSecuritySettingContents extends React.Component {
         </div>
 
         {isLocalEnabled && (
-          <React.Fragment>
+          <>
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
 
             <div className="row">
@@ -236,9 +234,9 @@ class LocalSecuritySettingContents extends React.Component {
                 </button>
               </div>
             </div>
-          </React.Fragment>
+          </>
         )}
-      </React.Fragment>
+      </>
     );
   }
 
@@ -246,18 +244,17 @@ class LocalSecuritySettingContents extends React.Component {
 
 LocalSecuritySettingContents.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
   adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer).isRequired,
 };
 
 const LocalSecuritySettingContentsWrapperFC = (props) => {
   const { t } = useTranslation();
-  return <LocalSecuritySettingContents t={t} {...props} />;
+  const { data: isMailerSetup } = useIsMailerSetup();
+  return <LocalSecuritySettingContents t={t} {...props} isMailerSetup={isMailerSetup ?? false} />;
 };
 
 const LocalSecuritySettingContentsWrapper = withUnstatedContainers(LocalSecuritySettingContentsWrapperFC, [
-  AppContainer,
   AdminGeneralSecurityContainer,
   AdminLocalSecurityContainer,
 ]);

+ 17 - 22
packages/app/src/components/Admin/Security/OidcSecuritySetting.jsx

@@ -1,5 +1,4 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
+import React, { useEffect, useCallback } from 'react';
 
 import PropTypes from 'prop-types';
 
@@ -11,29 +10,25 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import OidcSecurityManagementContents from './OidcSecuritySettingContents';
 
-let retrieveErrors = null;
-function OidcSecurityManagement(props) {
+const OidcSecurityManagement = (props) => {
   const { adminOidcSecurityContainer } = props;
-  if (adminOidcSecurityContainer.state.oidcProviderName === adminOidcSecurityContainer.dummyOidcProviderName) {
-    throw (async() => {
-      try {
-        await adminOidcSecurityContainer.retrieveSecurityData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        retrieveErrors = errs;
-        adminOidcSecurityContainer.setState({ oidcProviderName: adminOidcSecurityContainer.dummyOidcProviderNameForError });
-      }
-    })();
-  }
-
-  if (adminOidcSecurityContainer.state.oidcProviderName === adminOidcSecurityContainer.dummyOidcProviderNameForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  const fetchOidcSecuritySettingsData = useCallback(async() => {
+    try {
+      await adminOidcSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+    }
+  }, [adminOidcSecurityContainer]);
+
+  useEffect(() => {
+    fetchOidcSecuritySettingsData();
+  }, [adminOidcSecurityContainer, fetchOidcSecuritySettingsData]);
 
   return <OidcSecurityManagementContents />;
-}
+};
 
 OidcSecurityManagement.propTypes = {
   adminOidcSecurityContainer: PropTypes.instanceOf(AdminOidcSecurityContainer).isRequired,

+ 19 - 13
packages/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx

@@ -1,13 +1,15 @@
-/* eslint-disable react/no-danger */
 import React from 'react';
 
-import PropTypes from 'prop-types';
+import { pathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
+import urljoin from 'url-join';
 
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { useSiteUrl } from '~/stores/context';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -33,13 +35,15 @@ class OidcSecurityManagementContents extends React.Component {
   }
 
   render() {
-    const { t, adminGeneralSecurityContainer, adminOidcSecurityContainer } = this.props;
+    const {
+      t, adminGeneralSecurityContainer, adminOidcSecurityContainer, siteUrl,
+    } = this.props;
     const { isOidcEnabled } = adminGeneralSecurityContainer.state;
+    const oidcCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/oidc/callback');
 
     return (
 
-      <React.Fragment>
-
+      <>
         <h2 className="alert-anchor border-bottom">
           {t('security_setting.OAuth.OIDC.name')}
         </h2>
@@ -69,11 +73,11 @@ class OidcSecurityManagementContents extends React.Component {
             <input
               className="form-control"
               type="text"
-              value={adminOidcSecurityContainer.state.callbackUrl}
+              value={oidcCallbackUrl}
               readOnly
             />
             <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
-            {!adminGeneralSecurityContainer.state.appSiteUrl && (
+            {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
@@ -86,7 +90,7 @@ class OidcSecurityManagementContents extends React.Component {
         </div>
 
         {isOidcEnabled && (
-          <React.Fragment>
+          <>
 
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
 
@@ -365,11 +369,11 @@ class OidcSecurityManagementContents extends React.Component {
                 <input
                   className="form-control"
                   type="text"
-                  defaultValue={adminOidcSecurityContainer.state.callbackUrl || ''}
+                  defaultValue={oidcCallbackUrl}
                   readOnly
                 />
                 <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
-                {!adminGeneralSecurityContainer.state.appSiteUrl && (
+                {(siteUrl == null || siteUrl === '') && (
                   <div className="alert alert-danger">
                     <i
                       className="icon-exclamation"
@@ -437,7 +441,7 @@ class OidcSecurityManagementContents extends React.Component {
                 </button>
               </div>
             </div>
-          </React.Fragment>
+          </>
         )}
 
 
@@ -455,7 +459,7 @@ class OidcSecurityManagementContents extends React.Component {
           </ol>
         </div>
 
-      </React.Fragment>
+      </>
     );
   }
 
@@ -465,11 +469,13 @@ OidcSecurityManagementContents.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
   adminOidcSecurityContainer: PropTypes.instanceOf(AdminOidcSecurityContainer).isRequired,
+  siteUrl: PropTypes.string,
 };
 
 const OidcSecurityManagementContentsWrapperFC = (props) => {
   const { t } = useTranslation();
-  return <OidcSecurityManagementContents t={t} {...props} />;
+  const { data: siteUrl } = useSiteUrl();
+  return <OidcSecurityManagementContents t={t} {...props} siteUrl={siteUrl} />;
 };
 
 const OidcSecurityManagementContentsWrapper = withUnstatedContainers(OidcSecurityManagementContentsWrapperFC, [

+ 17 - 22
packages/app/src/components/Admin/Security/SamlSecuritySetting.jsx

@@ -1,5 +1,4 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
+import React, { useEffect, useCallback } from 'react';
 
 import PropTypes from 'prop-types';
 
@@ -11,29 +10,25 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import SamlSecuritySettingContents from './SamlSecuritySettingContents';
 
-let retrieveErrors = null;
-function SamlSecurityManagement(props) {
+const SamlSecurityManagement = (props) => {
   const { adminSamlSecurityContainer } = props;
-  if (adminSamlSecurityContainer.state.samlEntryPoint === adminSamlSecurityContainer.dummySamlEntryPoint) {
-    throw (async() => {
-      try {
-        await adminSamlSecurityContainer.retrieveSecurityData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        retrieveErrors = errs;
-        adminSamlSecurityContainer.setState({ samlEntryPoint: adminSamlSecurityContainer.dummySamlEntryPointForError });
-      }
-    })();
-  }
-
-  if (adminSamlSecurityContainer.state.samlEntryPoint === adminSamlSecurityContainer.dummySamlEntryPointForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  const fetchSamlSecuritySettingsData = useCallback(async() => {
+    try {
+      await adminSamlSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+    }
+  }, [adminSamlSecurityContainer]);
+
+  useEffect(() => {
+    fetchSamlSecuritySettingsData();
+  }, [adminSamlSecurityContainer, fetchSamlSecuritySettingsData]);
 
   return <SamlSecuritySettingContents />;
-}
+};
 
 SamlSecurityManagement.propTypes = {
   adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer).isRequired,

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

@@ -1,17 +1,21 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
 
-import PropTypes from 'prop-types';
+import { pathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import { Collapse } from 'reactstrap';
+import urljoin from 'url-join';
 
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { useSiteUrl } from '~/stores/context';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
+
 class SamlSecurityManagementContents extends React.Component {
 
   constructor(props) {
@@ -38,10 +42,14 @@ class SamlSecurityManagementContents extends React.Component {
   }
 
   render() {
-    const { t, adminGeneralSecurityContainer, adminSamlSecurityContainer } = this.props;
+    const {
+      t, adminGeneralSecurityContainer, adminSamlSecurityContainer, siteUrl,
+    } = this.props;
     const { useOnlyEnvVars } = adminSamlSecurityContainer.state;
     const { isSamlEnabled } = adminGeneralSecurityContainer.state;
 
+    const samlCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/saml/callback');
+
     return (
       <React.Fragment>
 
@@ -82,11 +90,11 @@ class SamlSecurityManagementContents extends React.Component {
             <input
               className="form-control"
               type="text"
-              defaultValue={adminSamlSecurityContainer.state.callbackUrl}
+              defaultValue={samlCallbackUrl}
               readOnly
             />
             <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'SAML Identity' })}</p>
-            {!adminGeneralSecurityContainer.state.appSiteUrl && (
+            {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
@@ -534,11 +542,13 @@ SamlSecurityManagementContents.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
   adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer).isRequired,
+  siteUrl: PropTypes.string,
 };
 
 const SamlSecurityManagementContentsWrapperFC = (props) => {
   const { t } = useTranslation();
-  return <SamlSecurityManagementContents t={t} {...props} />;
+  const { data: siteUrl } = useSiteUrl();
+  return <SamlSecurityManagementContents t={t} siteUrl={siteUrl} {...props} />;
 };
 
 const SamlSecurityManagementContentsWrapper = withUnstatedContainers(SamlSecurityManagementContentsWrapperFC, [

+ 14 - 21
packages/app/src/components/Admin/Security/SecurityManagement.jsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect, useCallback } from 'react';
 
 import PropTypes from 'prop-types';
 
@@ -10,29 +10,22 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import SecurityManagementContents from './SecurityManagementContents';
 
-let retrieveErrors = null;
 function SecurityManagement(props) {
   const { adminGeneralSecurityContainer } = props;
 
-  if (adminGeneralSecurityContainer.state.currentRestrictGuestMode === adminGeneralSecurityContainer.dummyCurrentRestrictGuestMode) {
-    throw (async() => {
-      try {
-        await adminGeneralSecurityContainer.retrieveSecurityData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        retrieveErrors = errs;
-        adminGeneralSecurityContainer.setState({
-          currentRestrictGuestMode: adminGeneralSecurityContainer.dummyCurrentRestrictGuestModeForError,
-        });
-      }
-    })();
-  }
-
-  if (adminGeneralSecurityContainer.state.currentRestrictGuestMode === adminGeneralSecurityContainer.dummyCurrentRestrictGuestModeForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+  const fetchGeneralSecuritySettingsData = useCallback(async() => {
+    try {
+      await adminGeneralSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+    }
+  }, [adminGeneralSecurityContainer]);
+
+  useEffect(() => {
+    fetchGeneralSecuritySettingsData();
+  }, [adminGeneralSecurityContainer, fetchGeneralSecuritySettingsData]);
 
   return <SecurityManagementContents />;
 }

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

@@ -5,17 +5,17 @@ import { TabContent, TabPane } from 'reactstrap';
 
 import CustomNav from '../../CustomNavigation/CustomNav';
 
-// import BasicSecuritySetting from './BasicSecuritySetting';
-// import FacebookSecuritySetting from './FacebookSecuritySetting';
-// import GitHubSecuritySetting from './GitHubSecuritySetting';
-// import GoogleSecuritySetting from './GoogleSecuritySetting';
-// import LdapSecuritySetting from './LdapSecuritySetting';
-// import LocalSecuritySetting from './LocalSecuritySetting';
-// import OidcSecuritySetting from './OidcSecuritySetting';
-// import SamlSecuritySetting from './SamlSecuritySetting';
+import BasicSecuritySetting from './BasicSecuritySetting';
+import FacebookSecuritySetting from './FacebookSecuritySetting';
+import GitHubSecuritySetting from './GitHubSecuritySetting';
+import GoogleSecuritySetting from './GoogleSecuritySetting';
+import LdapSecuritySetting from './LdapSecuritySetting';
+import LocalSecuritySetting from './LocalSecuritySetting';
+import OidcSecuritySetting from './OidcSecuritySetting';
+import SamlSecuritySetting from './SamlSecuritySetting';
 import SecuritySetting from './SecuritySetting';
 import ShareLinkSetting from './ShareLinkSetting';
-// import TwitterSecuritySetting from './TwitterSecuritySetting';
+import TwitterSecuritySetting from './TwitterSecuritySetting';
 
 const SecurityManagementContents = () => {
   const { t } = useTranslation();
@@ -112,31 +112,31 @@ const SecurityManagementContents = () => {
         />
         <TabContent activeTab={activeTab} className="p-5">
           <TabPane tabId="passport_local">
-            {/* {activeComponents.has('passport_local') && <LocalSecuritySetting />} */}
+            {activeComponents.has('passport_local') && <LocalSecuritySetting />}
           </TabPane>
           <TabPane tabId="passport_ldap">
-            {/* {activeComponents.has('passport_ldap') && <LdapSecuritySetting />} */}
+            {activeComponents.has('passport_ldap') && <LdapSecuritySetting />}
           </TabPane>
           <TabPane tabId="passport_saml">
-            {/* {activeComponents.has('passport_saml') && <SamlSecuritySetting />} */}
+            {activeComponents.has('passport_saml') && <SamlSecuritySetting />}
           </TabPane>
           <TabPane tabId="passport_oidc">
-            {/* {activeComponents.has('passport_oidc') && <OidcSecuritySetting />} */}
+            {activeComponents.has('passport_oidc') && <OidcSecuritySetting />}
           </TabPane>
           <TabPane tabId="passport_basic">
-            {/* {activeComponents.has('passport_basic') && <BasicSecuritySetting />} */}
+            {activeComponents.has('passport_basic') && <BasicSecuritySetting />}
           </TabPane>
           <TabPane tabId="passport_google">
-            {/* {activeComponents.has('passport_google') && <GoogleSecuritySetting />} */}
+            {activeComponents.has('passport_google') && <GoogleSecuritySetting />}
           </TabPane>
           <TabPane tabId="passport_github">
-            {/* {activeComponents.has('passport_github') && <GitHubSecuritySetting />} */}
+            {activeComponents.has('passport_github') && <GitHubSecuritySetting />}
           </TabPane>
           <TabPane tabId="passport_twitter">
-            {/* {activeComponents.has('passport_twitter') && <TwitterSecuritySetting />} */}
+            {activeComponents.has('passport_twitter') && <TwitterSecuritySetting />}
           </TabPane>
           <TabPane tabId="passport_facebook">
-            {/* {activeComponents.has('passport_facebook') && <FacebookSecuritySetting />} */}
+            {activeComponents.has('passport_facebook') && <FacebookSecuritySetting />}
           </TabPane>
         </TabContent>
       </div>

+ 17 - 25
packages/app/src/components/Admin/Security/TwitterSecuritySetting.jsx

@@ -1,5 +1,4 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
+import React, { useEffect, useCallback } from 'react';
 
 import PropTypes from 'prop-types';
 
@@ -9,34 +8,27 @@ import { toArrayIfNot } from '~/utils/array-utils';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
-
 import TwitterSecuritySettingContents from './TwitterSecuritySettingContents';
 
-let retrieveErrors = null;
-function TwitterSecurityManagement(props) {
+const TwitterSecurityManagement = (props) => {
   const { adminTwitterSecurityContainer } = props;
-  if (adminTwitterSecurityContainer.state.twitterConsumerKey === adminTwitterSecurityContainer.dummyTwitterConsumerKey) {
-    throw (async() => {
-      try {
-        await adminTwitterSecurityContainer.retrieveSecurityData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        retrieveErrors = errs;
-        adminTwitterSecurityContainer.setState({
-          twitterConsumerKey: adminTwitterSecurityContainer.dummyTwitterConsumerKeyForError,
-        });
-      }
-    })();
-  }
-
-  if (adminTwitterSecurityContainer.state.twitterConsumerKey === adminTwitterSecurityContainer.dummyTwitterConsumerKeyForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  const fetchTwitterSecuritySettingsData = useCallback(async() => {
+    try {
+      await adminTwitterSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+    }
+  }, [adminTwitterSecurityContainer]);
+
+  useEffect(() => {
+    fetchTwitterSecuritySettingsData();
+  }, [adminTwitterSecurityContainer, fetchTwitterSecuritySettingsData]);
 
   return <TwitterSecuritySettingContents />;
-}
+};
 
 TwitterSecurityManagement.propTypes = {
   adminTwitterSecurityContainer: PropTypes.instanceOf(AdminTwitterSecurityContainer).isRequired,

+ 13 - 5
packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx

@@ -1,13 +1,16 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
 
-import PropTypes from 'prop-types';
+import { pathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
+import urljoin from 'url-join';
 
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { useSiteUrl } from '~/stores/context';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -33,8 +36,11 @@ class TwitterSecuritySettingContents extends React.Component {
   }
 
   render() {
-    const { t, adminGeneralSecurityContainer, adminTwitterSecurityContainer } = this.props;
+    const {
+      t, adminGeneralSecurityContainer, adminTwitterSecurityContainer, siteUrl,
+    } = this.props;
     const { isTwitterEnabled } = adminGeneralSecurityContainer.state;
+    const twitterCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/twitter/callback');
 
     return (
 
@@ -75,11 +81,11 @@ class TwitterSecuritySettingContents extends React.Component {
             <input
               className="form-control"
               type="text"
-              value={adminTwitterSecurityContainer.state.callbackUrl}
+              value={twitterCallbackUrl}
               readOnly
             />
             <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
-            {!adminGeneralSecurityContainer.state.appSiteUrl && (
+            {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
@@ -197,11 +203,13 @@ TwitterSecuritySettingContents.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
   adminTwitterSecurityContainer: PropTypes.instanceOf(AdminTwitterSecurityContainer).isRequired,
+  siteUrl: PropTypes.string,
 };
 
 const TwitterSecuritySettingContentsWrapperFC = (props) => {
   const { t } = useTranslation();
-  return <TwitterSecuritySettingContents t={t} {...props} />;
+  const { data: siteUrl } = useSiteUrl();
+  return <TwitterSecuritySettingContents t={t} siteUrl={siteUrl} {...props} />;
 };
 
 /**

+ 10 - 9
packages/app/src/components/LoginForm.jsx

@@ -4,11 +4,8 @@ import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import ReactCardFlip from 'react-card-flip';
 
-import AppContainer from '~/client/services/AppContainer';
 import { useCsrfToken } from '~/stores/context';
 
-import { withUnstatedContainers } from './UnstatedUtils';
-
 class LoginForm extends React.Component {
 
   constructor(props) {
@@ -148,7 +145,7 @@ class LoginForm extends React.Component {
   renderRegisterForm() {
     const {
       t,
-      appContainer,
+      // appContainer,
       csrfToken,
       isEmailAuthenticationEnabled,
       username,
@@ -156,9 +153,9 @@ class LoginForm extends React.Component {
       email,
       registrationMode,
       registrationWhiteList,
+      isMailerSetup,
     } = this.props;
 
-    const { isMailerSetup } = appContainer.config;
     let registerAction = '/register';
 
     let submitText = t('Sign up');
@@ -288,8 +285,10 @@ class LoginForm extends React.Component {
       objOfIsExternalAuthEnableds,
     } = this.props;
 
+
     const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
-    const isSomeExternalAuthEnabled = Object.values(objOfIsExternalAuthEnableds).some(elem => elem);
+    // const isSomeExternalAuthEnabled = Object.values(objOfIsExternalAuthEnableds).some(elem => elem);
+    const isSomeExternalAuthEnabled = true;
 
     return (
       <div className="login-dialog mx-auto" id="login-dialog">
@@ -332,7 +331,7 @@ class LoginForm extends React.Component {
 LoginForm.propTypes = {
   // i18next
   t: PropTypes.func.isRequired,
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  // appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   csrfToken: PropTypes.string,
   isRegistering: PropTypes.bool,
@@ -347,6 +346,7 @@ LoginForm.propTypes = {
   isLocalStrategySetup: PropTypes.bool,
   isLdapStrategySetup: PropTypes.bool,
   objOfIsExternalAuthEnableds: PropTypes.object,
+  isMailerSetup: PropTypes.bool,
 };
 
 const LoginFormWrapperFC = (props) => {
@@ -359,6 +359,7 @@ const LoginFormWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const LoginFormWrapper = withUnstatedContainers(LoginFormWrapperFC, [AppContainer]);
+// const LoginFormWrapper = withUnstatedContainers(LoginFormWrapperFC, [AppContainer]);
 
-export default LoginFormWrapper;
+// export default LoginForm;
+export default LoginFormWrapperFC;

+ 3 - 5
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -7,7 +7,7 @@ import { TabContent, TabPane } from 'reactstrap';
 
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import {
-  useCurrentPagePath, useIsSharedUser, useIsEditable, useIsUserPage, usePageUser, useShareLinkId, useIsNotFound, useIsNotCreatable,
+  useCurrentPagePath, useIsSharedUser, useIsEditable, useIsUserPage, usePageUser, useShareLinkId, useIsNotFound,
 } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
@@ -15,7 +15,7 @@ import { EditorMode, useEditorMode } from '~/stores/ui';
 
 import CountBadge from '../Common/CountBadge';
 import PageListIcon from '../Icons/PageListIcon';
-import { NotCreatablePage } from '../NotCreatablePage';
+import NotFoundPage from '../NotFoundPage';
 import { Page } from '../Page';
 // import PageEditor from '../PageEditor';
 // import PageEditorByHackmd from '../PageEditorByHackmd';
@@ -48,7 +48,6 @@ const DisplaySwitcher = (): JSX.Element => {
   const { data: isEditable } = useIsEditable();
   const { data: pageUser } = usePageUser();
   const { data: isNotFound } = useIsNotFound();
-  const { data: isNotCreatable } = useIsNotCreatable();
   const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
 
   const { data: editorMode } = useEditorMode();
@@ -69,8 +68,7 @@ const DisplaySwitcher = (): JSX.Element => {
             <div className="flex-grow-1 flex-basis-0 mw-0">
               { isUserPage && <UserInfo pageUser={pageUser} />}
               { !isNotFound && <Page /> }
-              { isNotFound && !isNotCreatable && <NotFoundPage /> }
-              { isNotFound && isNotCreatable && <NotCreatablePage /> }
+              { isNotFound && <NotFoundPage /> }
             </div>
 
             { !isNotFound && !currentPage?.isEmpty && (

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

@@ -2,7 +2,7 @@
 
 import React from 'react';
 
-import { Subscribe } from 'unstated';
+import { Provider, Subscribe } from 'unstated';
 
 /**
  * generate K/V object by specified instances
@@ -15,6 +15,7 @@ import { Subscribe } from 'unstated';
  *     exampleContainer: <ExampleContainer />,
  *   }
  */
+
 function generateAutoNamedProps(instances) {
   const props = {};
 

+ 5 - 4
packages/app/src/migrations/20211129125654-initialize-private-legacy-pages-named-query.js

@@ -13,10 +13,11 @@ module.exports = {
     mongoose.connect(getMongoUri(), mongoOptions);
 
     try {
-      await NamedQuery.insertMany({
-        name: SearchDelegatorName.PRIVATE_LEGACY_PAGES,
-        delegatorName: SearchDelegatorName.PRIVATE_LEGACY_PAGES,
-      });
+      await NamedQuery.updateOne(
+        { name: SearchDelegatorName.PRIVATE_LEGACY_PAGES },
+        { delegatorName: SearchDelegatorName.PRIVATE_LEGACY_PAGES },
+        { upsert: true },
+      );
     }
     catch (err) {
       logger.error('Failed to migrate named query for private legacy pages search delagator.', err);

+ 22 - 14
packages/app/src/pages/[[...path]].page.tsx

@@ -7,6 +7,7 @@ import {
   IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, isClient, isIPageInfoForEntity, isServer, IUser, IUserHasId, pagePathUtils, pathUtils,
 } from '@growi/core';
 import ExtensibleCustomError from 'extensible-custom-error';
+import mongoose from 'mongoose';
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
@@ -29,6 +30,7 @@ import { RendererConfig } from '~/interfaces/services/renderer';
 import { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
 import { PageModel, PageDocument } from '~/server/models/page';
+import { PageRedirectModel } from '~/server/models/page-redirect';
 import UserUISettings from '~/server/models/user-ui-settings';
 import Xss from '~/services/xss';
 import { useSWRxCurrentPage, useSWRxPageInfo } from '~/stores/page';
@@ -41,8 +43,10 @@ import loggerFactory from '~/utils/logger';
 
 // import GrowiSubNavigation from '../client/js/components/Navbar/GrowiSubNavigation';
 // import GrowiSubNavigationSwitcher from '../client/js/components/Navbar/GrowiSubNavigationSwitcher';
+import ForbiddenPage from '../components/ForbiddenPage';
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import GrowiContextualSubNavigation from '../components/Navbar/GrowiContextualSubNavigation';
+import { NotCreatablePage } from '../components/NotCreatablePage';
 import DisplaySwitcher from '../components/Page/DisplaySwitcher';
 
 // import { serializeUserSecurely } from '../server/models/serializers/user-serializer';
@@ -65,7 +69,6 @@ import { useXss } from '../stores/xss';
 import {
   CommonProps, getNextI18NextConfig, getServerSideCommonProps, useCustomTitle,
 } from './utils/commons';
-import { registerTransformerForObjectId } from './utils/objectid-transformer';
 // import { useCurrentPageSWR } from '../stores/page';
 
 
@@ -80,9 +83,6 @@ const { removeHeadingSlash } = pathUtils;
 type IPageToShowRevisionWithMeta = IDataWithMeta<IPagePopulatedToShowRevision & PageDocument, IPageInfoForEntity>;
 type IPageToShowRevisionWithMetaSerialized = IDataWithMeta<string, string>;
 
-// register custom serializer
-registerTransformerForObjectId();
-
 superjson.registerCustom<IPageToShowRevisionWithMeta, IPageToShowRevisionWithMetaSerialized>(
   {
     isApplicable: (v): v is IPageToShowRevisionWithMeta => {
@@ -123,8 +123,7 @@ type Props = CommonProps & {
 
   pageWithMeta: IPageToShowRevisionWithMeta,
   // pageUser?: any,
-  // redirectTo?: string;
-  // redirectFrom?: string;
+  redirectFrom?: string;
 
   // shareLinkId?: string;
   isLatestRevision?: boolean
@@ -302,10 +301,10 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
                 { !props.isIdenticalPathPage && (
                   <>
                     <PageAlerts />
-                    { props.isForbidden
-                      ? <>ForbiddenPage</>
-                      : <DisplaySwitcher />
-                    }
+                    { props.isForbidden && <ForbiddenPage /> }
+                    { props.IsNotCreatable && <NotCreatablePage />}
+                    { !props.isForbidden && !props.IsNotCreatable && <DisplaySwitcher />}
+                    {/* <DisplaySwitcher /> */}
                     <div id="page-editor-navbar-bottom-container" className="d-none d-edit-block"></div>
                     {/* <PageStatusAlert /> */}
                     PageStatusAlert
@@ -357,17 +356,28 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
   const { revisionId } = req.query;
 
   const Page = crowi.model('Page') as PageModel;
+  const PageRedirect = mongoose.model('PageRedirect') as PageRedirectModel;
   const { pageService } = crowi;
 
-  const { currentPathname } = props;
+  let currentPathname = props.currentPathname;
 
   const pageId = getPageIdFromPathname(currentPathname);
   const isPermalink = _isPermalink(currentPathname);
 
   const { user } = req;
 
-  // check whether the specified page path hits to multiple pages
   if (!isPermalink) {
+    // check redirects
+    const chains = await PageRedirect.retrievePageRedirectEndpoints(currentPathname);
+    if (chains != null) {
+      // overwrite currentPathname
+      currentPathname = chains.end.toPath;
+      props.currentPathname = currentPathname;
+      // set redirectFrom
+      props.redirectFrom = chains.start.fromPath;
+    }
+
+    // check whether the specified page path hits to multiple pages
     const count = await Page.countByPathAndViewer(currentPathname, user, null, true);
     if (count > 1) {
       throw new MultiplePagesHitsError(currentPathname);
@@ -413,9 +423,7 @@ async function injectRoutingInformation(context: GetServerSidePropsContext, prop
   }
   else if (page == null) {
     props.isNotFound = true;
-
     props.IsNotCreatable = !isCreatablePage(currentPathname);
-
     // check the page is forbidden or just does not exist.
     const count = isPermalink ? await Page.count({ _id: pageId }) : await Page.count({ path: currentPathname });
     props.isForbidden = count > 0;

+ 3 - 0
packages/app/src/pages/_app.page.tsx

@@ -18,6 +18,7 @@ import {
 } from '../stores/context';
 
 import { CommonProps } from './utils/commons';
+import { registerTransformerForObjectId } from './utils/objectid-transformer';
 // import { useInterceptorManager } from '~/stores/interceptor';
 
 const isDev = process.env.NODE_ENV === 'development';
@@ -25,6 +26,8 @@ const isDev = process.env.NODE_ENV === 'development';
 type GrowiAppProps = AppProps & {
   pageProps: CommonProps;
 };
+// register custom serializer
+registerTransformerForObjectId();
 
 function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   useI18nextHMR(isDev);

+ 166 - 6
packages/app/src/pages/_search.page.tsx

@@ -1,26 +1,186 @@
 import {
-  NextPage, GetServerSideProps,
+  NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 
-const SearchPage: NextPage = () => {
+import { BasicLayout } from '~/components/Layout/BasicLayout';
+import { CrowiRequest } from '~/interfaces/crowi-request';
+import { RendererConfig } from '~/interfaces/services/renderer';
+import { ISidebarConfig } from '~/interfaces/sidebar-config';
+import { IUser, IUserHasId } from '~/interfaces/user';
+import { IUserUISettings } from '~/interfaces/user-ui-settings';
+import UserUISettings from '~/server/models/user-ui-settings';
+import Xss from '~/services/xss';
+import {
+  useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
+  useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig,
+} from '~/stores/context';
+import {
+  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
+  useCurrentSidebarContents, useCurrentProductNavWidth,
+} from '~/stores/ui';
+import { useXss } from '~/stores/xss';
+
+import {
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, useCustomTitle,
+} from './utils/commons';
+
+type Props = CommonProps & {
+  currentUser: IUser,
+
+  isSearchServiceConfigured: boolean,
+  isSearchServiceReachable: boolean,
+  isSearchScopeChildrenAsDefault: boolean,
+
+  // UI
+  userUISettings?: IUserUISettings
+  // Sidebar
+  sidebarConfig: ISidebarConfig,
+
+  // Render config
+  rendererConfig: RendererConfig,
+
+};
+
+const SearchPage: NextPage<Props> = (props: Props) => {
+  const { userUISettings } = props;
+
+  // commons
+  useXss(new Xss());
+  useCsrfToken(props.csrfToken);
+
+  useCurrentUser(props.currentUser ?? null);
+
+  // Search
+  useIsSearchPage(true);
+  useIsSearchServiceConfigured(props.isSearchServiceConfigured);
+  useIsSearchServiceReachable(props.isSearchServiceReachable);
+  useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
+
+  // UserUISettings
+  usePreferDrawerModeByUser(userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
+  usePreferDrawerModeOnEditByUser(userUISettings?.preferDrawerModeOnEditByUser);
+  useSidebarCollapsed(userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
+  useCurrentSidebarContents(userUISettings?.currentSidebarContents);
+  useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
+
+  // render config
+  useRendererConfig(props.rendererConfig);
 
   const PutbackPageModal = (): JSX.Element => {
     const PutbackPageModal = dynamic(() => import('../components/PutbackPageModal'), { ssr: false });
     return <PutbackPageModal />;
   };
 
+  const classNames: string[] = [];
+  // if (props.isContainerFluid) {
+  //   classNames.push('growi-layout-fluid');
+  // }
+
   return (
     <>
-      SearchPage
-      <PutbackPageModal />
+      <Head>
+        {/*
+        {renderScriptTagByName('drawio-viewer')}
+        {renderScriptTagByName('highlight-addons')}
+        */}
+      </Head>
+      <BasicLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')}>
+
+        <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
+        <div id="main" className="main search-page mt-0">
+
+          <div id="search-page">
+            Search Result Page
+            {/* render SearchPage component here */}
+          </div>
+
+        </div>
+        <PutbackPageModal />
+      </BasicLayout>
+
     </>
   );
 };
 
-export const getServerSideProps: GetServerSideProps = async() => {
+async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const req = context.req as CrowiRequest<IUserHasId & any>;
+  const { user } = req;
+
+  const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
+  if (userUISettings != null) {
+    props.userUISettings = userUISettings.toObject();
+  }
+}
+
+function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { crowi } = req;
+  const { configManager, searchService } = crowi;
+
+  props.isSearchServiceConfigured = searchService.isConfigured;
+  props.isSearchServiceReachable = searchService.isReachable;
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
+
+  props.sidebarConfig = {
+    isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+  };
+
+  props.rendererConfig = {
+    isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
+    isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+    adminPreferredIndentSize: configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
+    isIndentSizeForced: configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
+
+    plantumlUri: process.env.PLANTUML_URI ?? null,
+    blockdiagUri: process.env.BLOCKDIAG_URI ?? null,
+
+    // XSS Options
+    isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
+    attrWhiteList: crowi.xssService.getAttrWhiteList(),
+    tagWhiteList: crowi.xssService.getTagWhiteList(),
+    highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+  };
+}
+
+/**
+ * for Server Side Translations
+ * @param context
+ * @param props
+ * @param namespacesRequired
+ */
+async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  props._nextI18Next = nextI18NextConfig._nextI18Next;
+}
+
+export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+  const req = context.req as CrowiRequest<IUserHasId & any>;
+  const { user } = req;
+
+  const result = await getServerSideCommonProps(context);
+
+  // check for presence
+  // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
+  if (!('props' in result)) {
+    throw new Error('invalid getSSP result');
+  }
+
+  const props: Props = result.props as Props;
+
+  if (user != null) {
+    props.currentUser = user.toObject();
+  }
+
+  await injectUserUISettings(context, props);
+  injectServerConfigurations(context, props);
+  await injectNextI18NextConfigurations(context, props, ['translation']);
+
   return {
-    props: { },
+    props,
   };
 };
 

+ 13 - 1
packages/app/src/pages/admin/[[...path]].page.tsx

@@ -12,7 +12,7 @@ import { CrowiRequest } from '~/interfaces/crowi-request';
 import PluginUtils from '~/server/plugins/plugin-utils';
 import ConfigLoader from '~/server/service/config-loader';
 import {
-  useCurrentUser, /* useSearchServiceConfigured, */ useIsSearchServiceReachable, useSiteUrl,
+  useCurrentUser, /* useSearchServiceConfigured, */ useIsMailerSetup, useIsSearchServiceReachable, useSiteUrl,
 } from '~/stores/context';
 
 import {
@@ -54,6 +54,7 @@ type Props = CommonProps & {
 
   isSearchServiceConfigured: boolean,
   isSearchServiceReachable: boolean,
+  isMailerSetup: boolean,
 
   siteUrl: string,
 };
@@ -138,6 +139,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
   const title = content.title;
 
   useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
+  useIsMailerSetup(props.isMailerSetup);
 
   // useSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
@@ -172,6 +174,15 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
   );
 };
 
+
+function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { crowi } = req;
+  const { mailService } = crowi;
+
+  props.isMailerSetup = mailService.isMailerSetup;
+}
+
 /**
  * for Server Side Translations
  * @param context
@@ -204,6 +215,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     props.currentUser = JSON.stringify(user);
   }
 
+  injectServerConfigurations(context, props);
   injectNextI18NextConfigurations(context, props, ['admin']);
 
   props.siteUrl = appService.getSiteUrl();

+ 20 - 9
packages/app/src/pages/installer.page.tsx

@@ -48,18 +48,29 @@ const InstallerPage: NextPage<Props> = (props: Props) => {
   return (
     <>
       <RawLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')}>
-        <div id="page-wrapper">
-          <div className="main container-fluid">
+        <div className='nologin'>
+          <div id="wrapper">
+            <div id="page-wrapper">
+              <div className="main container-fluid">
+
+                <div className="row">
+
+                  <div className="col-md-12">
+                    <div className="login-header mx-auto">
+                      <div className="logo"></div>
+                      <h1 className="my-3">GROWI</h1>
+                      <div className="login-form-errors px-3"></div>
+                    </div>
+                  </div>
+
+                  <div className="col-md-12">
+                    <div id="installer-form-container">
+                      <InstallerForm />
+                    </div>
+                  </div>
 
-            <div className="row">
-              <div className="col-md-12">
-                <div className="login-header mx-auto">
-                  <h1 className="my-3">GROWI</h1>
                 </div>
               </div>
-              <div className="col-md-12">
-                <InstallerForm />
-              </div>
             </div>
           </div>
         </div>

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

@@ -0,0 +1,112 @@
+import React from 'react';
+
+
+import { pagePathUtils } from '@growi/core';
+import {
+  NextPage, GetServerSideProps, GetServerSidePropsContext,
+} from 'next';
+import dynamic from 'next/dynamic';
+
+import { RawLayout } from '~/components/Layout/RawLayout';
+import { CrowiRequest } from '~/interfaces/crowi-request';
+
+import {
+  useCsrfToken,
+  useCurrentPathname,
+} from '../stores/context';
+
+
+import {
+  CommonProps, getServerSideCommonProps, useCustomTitle,
+} from './utils/commons';
+
+type Props = CommonProps & {
+
+  pageWithMetaStr: string,
+  isMailerSetup: boolean,
+  enabledStrategies: unknown,
+  registrationWhiteList: string[],
+};
+
+const LoginPage: NextPage<Props> = (props: Props) => {
+
+  // commons
+  useCsrfToken(props.csrfToken);
+
+  // page
+  useCurrentPathname(props.currentPathname);
+
+  const classNames: string[] = [];
+
+  const LoginForm = dynamic(() => import('~/components/LoginForm'), {
+    ssr: false,
+  });
+
+  return (
+    <>
+      <RawLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')}>
+        <div className='nologin'>
+          <div id='wrapper'>
+            <div id="page-wrapper">
+              <LoginForm objOfIsExternalAuthEnableds={props.enabledStrategies} isLocalStrategySetup={true} isLdapStrategySetup={true}
+                isRegistrationEnabled={true} registrationWhiteList={props.registrationWhiteList} isPasswordResetEnabled={true} />
+            </div>
+          </div>
+        </div>
+      </RawLayout>
+    </>
+  );
+};
+
+function injectEnabledStrategies(context: GetServerSidePropsContext, props: Props): void {
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { crowi } = req;
+  const {
+    configManager,
+  } = crowi;
+
+  const enabledStrategies = {
+    google: configManager.getConfig('crowi', 'security:passport-google:isEnabled'),
+    github: configManager.getConfig('crowi', 'security:passport-github:isEnabled'),
+    facebook: false,
+    twitter: configManager.getConfig('crowi', 'security:passport-twitter:isEnabled'),
+    smal: configManager.getConfig('crowi', 'security:passport-saml:isEnabled'),
+    oidc: configManager.getConfig('crowi', 'security:passport-oidc:isEnabled'),
+    basic: configManager.getConfig('crowi', 'security:passport-basic:isEnabled'),
+  };
+
+  props.enabledStrategies = enabledStrategies;
+}
+
+async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { crowi } = req;
+  const {
+    mailService,
+    configManager,
+  } = crowi;
+
+  props.isMailerSetup = mailService.isMailerSetup;
+  props.registrationWhiteList = configManager.getConfig('crowi', 'security:registrationWhiteList');
+}
+
+export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+  const result = await getServerSideCommonProps(context);
+
+  // check for presence
+  // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
+  if (!('props' in result)) {
+    throw new Error('invalid getSSP result');
+  }
+
+  const props: Props = result.props as Props;
+
+  injectServerConfigurations(context, props);
+  injectEnabledStrategies(context, props);
+
+  return {
+    props,
+  };
+};
+
+export default LoginPage;

+ 133 - 4
packages/app/src/server/models/page-redirect.ts

@@ -4,19 +4,37 @@ import {
   Schema, Model, Document,
 } from 'mongoose';
 
+import loggerFactory from '~/utils/logger';
+
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-export interface IPageRedirect {
+
+const logger = loggerFactory('growi:models:page-redirects');
+
+
+export type IPageRedirect = {
   fromPath: string,
   toPath: string,
 }
 
+export type IPageRedirectEndpoints = {
+  start: IPageRedirect,
+  end: IPageRedirect,
+}
+
 export interface PageRedirectDocument extends IPageRedirect, Document {}
 
 export interface PageRedirectModel extends Model<PageRedirectDocument> {
-  [x:string]: any // TODO: improve type
+  retrievePageRedirectEndpoints(fromPath: string): Promise<IPageRedirectEndpoints>
+  removePageRedirectsByToPath(toPath: string): Promise<void>
 }
 
+const CHAINS_FIELD_NAME = 'chains';
+const DEPTH_FIELD_NAME = 'depth';
+type IPageRedirectWithChains = PageRedirectDocument & {
+  [CHAINS_FIELD_NAME]: (PageRedirectDocument & { [DEPTH_FIELD_NAME]: number })[]
+};
+
 const schema = new Schema<PageRedirectDocument, PageRedirectModel>({
   fromPath: {
     type: String, required: true, unique: true, index: true,
@@ -24,9 +42,120 @@ const schema = new Schema<PageRedirectDocument, PageRedirectModel>({
   toPath: { type: String, required: true },
 });
 
-schema.statics.removePageRedirectByToPath = async function(toPath: string): Promise<void> {
-  await this.deleteMany({ toPath });
+schema.statics.retrievePageRedirectEndpoints = async function(fromPath: string): Promise<IPageRedirectEndpoints|null> {
+  const aggResult: IPageRedirectWithChains[] = await this.aggregate([
+    { $match: { fromPath } },
+    {
+      $graphLookup: {
+        from: 'pageredirects',
+        startWith: '$toPath',
+        connectFromField: 'toPath',
+        connectToField: 'fromPath',
+        as: CHAINS_FIELD_NAME,
+        depthField: DEPTH_FIELD_NAME,
+      },
+    },
+  ]);
+  /* ---------- aggResult example ----------
+  {
+    "_id" : ObjectId("62e5650d6134d37aa0935e6d"),
+    "fromPath" : "/page1",
+    "toPath" : "/page2",
+    "chains" : [
+        {
+            "_id" : ObjectId("62e5651b6134d37aa0935e7a"),
+            "fromPath" : "/page2",
+            "toPath" : "/page3",
+            "depth" : NumberLong(0)
+        },
+        {
+            "_id" : ObjectId("62e565256134d37aa0935e80"),
+            "fromPath" : "/page3",
+            "toPath" : "/Sandbox",
+            "depth" : NumberLong(1)
+        }
+    ]
+  }
+  */
+
+  if (aggResult.length === 0) {
+    return null;
+  }
+
+  if (aggResult.length > 1) {
+    logger.warn(`Although two or more PageRedirect documents starts from '${fromPath}' exists, The first one is used.`);
+  }
+
+  const redirectWithChains = aggResult[0];
+
+  // sort chains in desc
+  const sortedChains = redirectWithChains[CHAINS_FIELD_NAME].sort((a, b) => b[DEPTH_FIELD_NAME] - a[DEPTH_FIELD_NAME]);
+
+  const start = { fromPath: redirectWithChains.fromPath, toPath: redirectWithChains.toPath };
+  const end = sortedChains.length === 0
+    ? start
+    : sortedChains[0];
+
+  return { start, end };
+};
+
+schema.statics.removePageRedirectsByToPath = async function(toPath: string): Promise<void> {
+  const aggResult: IPageRedirectWithChains[] = await this.aggregate([
+    { $match: { toPath } },
+    {
+      $graphLookup: {
+        from: 'pageredirects',
+        startWith: '$fromPath',
+        connectFromField: 'fromPath',
+        connectToField: 'toPath',
+        as: CHAINS_FIELD_NAME,
+      },
+    },
+  ]);
+  /* ---------- aggResult example ----------
+  // 1
+  {
+    "_id" : ObjectId("62e565256134d37aa0935e80"),
+    "fromPath" : "/page3",
+    "toPath" : "/page4",
+    "chains" : [
+        {
+            "_id" : ObjectId("62e5651b6134d37aa0935e7a"),
+            "fromPath" : "/page2",
+            "toPath" : "/page3",
+            "depth" : NumberLong(0)
+        },
+        {
+            "_id" : ObjectId("62e5650d6134d37aa0935e6d"),
+            "fromPath" : "/page1",
+            "toPath" : "/page2",
+            "depth" : NumberLong(1)
+        }
+    ]
+  }
+  // 2
+  {
+    "_id" : ObjectId("62e5937a6134d37aa0936405"),
+    "fromPath" : "/org/page4",
+    "toPath" : "/page4",
+    "chains" : []
+  }
+  */
+
+  if (aggResult.length === 0) {
+    return;
+  }
+
+  const idsToRemove = aggResult
+    .map((redirectWithChains) => {
+      return [
+        redirectWithChains._id,
+        redirectWithChains[CHAINS_FIELD_NAME].map(doc => doc._id),
+      ].flat();
+    })
+    .flat();
 
+  await this.deleteMany({ _id: { $in: idsToRemove } });
   return;
 };
 

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

@@ -79,7 +79,7 @@ module.exports = function(crowi, app) {
   app.get('/'                         , applicationInstalled, unavailableWhenMaintenanceMode, loginRequired, autoReconnectToSearch, next.delegateToNext);
 
   app.get('/login/error/:reason'      , applicationInstalled, login.error);
-  app.get('/login'                    , applicationInstalled, login.preLogin, login.login);
+  app.get('/login'                    , applicationInstalled, login.preLogin, next.delegateToNext);
   app.get('/login/invited'            , applicationInstalled, login.invited);
   app.post('/login/activateInvited'   , applicationInstalled, loginFormValidator.inviteRules(), loginFormValidator.inviteValidation, csrfProtection, login.invited);
   app.post('/login'                   , applicationInstalled, loginFormValidator.loginRules(), loginFormValidator.loginValidation, csrfProtection,  addActivity, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.loginFailure);

+ 12 - 12
packages/app/src/server/routes/login.js

@@ -67,18 +67,18 @@ module.exports = function(crowi, app) {
 
   actions.preLogin = function(req, res, next) {
     // user has already logged in
-    const { user } = req;
-    if (user != null && user.status === User.STATUS_ACTIVE) {
-      const { redirectTo } = req.session;
-      // remove session.redirectTo
-      delete req.session.redirectTo;
-      return res.safeRedirect(redirectTo);
-    }
-
-    // set referer to 'redirectTo'
-    if (req.session.redirectTo == null && req.headers.referer != null) {
-      req.session.redirectTo = req.headers.referer;
-    }
+    // const { user } = req;
+    // if (user != null && user.status === User.STATUS_ACTIVE) {
+    //   const { redirectTo } = req.session;
+    //   // remove session.redirectTo
+    //   delete req.session.redirectTo;
+    //   return res.safeRedirect(redirectTo);
+    // }
+
+    // // set referer to 'redirectTo'
+    // if (req.session.redirectTo == null && req.headers.referer != null) {
+    //   req.session.redirectTo = req.headers.referer;
+    // }
 
     next();
   };

+ 1 - 1
packages/app/src/server/routes/page.js

@@ -1476,7 +1476,7 @@ module.exports = function(crowi, app) {
     const path = req.body.path;
 
     try {
-      await PageRedirect.removePageRedirectByToPath(path);
+      await PageRedirect.removePageRedirectsByToPath(path);
       logger.debug('Redirect Page deleted', path);
     }
     catch (err) {

+ 4 - 0
packages/app/src/stores/context.tsx

@@ -172,6 +172,10 @@ export const useIsSearchServiceReachable = (initialData?: boolean) : SWRResponse
   return useStaticSWR<boolean, Error>('isSearchServiceReachable', initialData);
 };
 
+export const useIsMailerSetup = (initialData?: boolean): SWRResponse<boolean, any> => {
+  return useStaticSWR('isMailerSetup', initialData);
+};
+
 export const useIsSearchScopeChildrenAsDefault = (initialData?: boolean) : SWRResponse<boolean, Error> => {
   return useStaticSWR<boolean, Error>('isSearchScopeChildrenAsDefault', initialData);
 };

+ 13 - 10
packages/app/src/styles/_login.scss

@@ -1,3 +1,6 @@
+@use '~/styles/bootstrap/init' as bs;
+
+
 .nologin {
   #page-wrapper {
     background: none;
@@ -92,40 +95,40 @@
 
   $btn-fill-colors: (
     'login': (
-      rgba($danger, 0.4),
+      rgba(bs.$danger, 0.4),
       rgba(#7e4153, 0.7),
     ),
     'register': (
-      rgba($success, 0.4),
+      rgba(bs.$success, 0.4),
       rgba(#3f7263, 0.7),
     ),
     'google': (
       rgba(#24292e, 0.4),
-      $gray-700,
+      bs.$gray-700,
     ),
     'github': (
       rgba(lighten(black, 20%), 0.4),
-      $gray-700,
+      bs.$gray-700,
     ),
     'facebook': (
       rgba(#29487d, 0.4),
-      $gray-700,
+      bs.$gray-700,
     ),
     'twitter': (
       rgba(#1da1f2, 0.4),
-      $gray-700,
+      bs.$gray-700,
     ),
     'oidc': (
       rgba(#24292e, 0.4),
-      $gray-700,
+      bs.$gray-700,
     ),
     'saml': (
       rgba(#55a79a, 0.4),
-      $gray-700,
+      bs.$gray-700,
     ),
     'basic': (
       rgba(#24292e, 0.4),
-      $gray-700,
+      bs.$gray-700,
     ),
   );
 
@@ -153,7 +156,7 @@
   }
 
   .link-switch {
-    color: $gray-200;
+    color: bs.$gray-200;
 
     &:hover {
       color: white;

+ 1 - 1
packages/app/src/styles/style-next.scss

@@ -46,7 +46,7 @@
 // @import 'page-content-footer';
 // @import 'handsontable';
 @import 'layout';
-// @import 'login';
+@import 'login';
 // @import 'me';
 // @import 'mirror_mode';
 // @import 'modal';

+ 111 - 0
packages/app/test/integration/models/page-redirect.test.js

@@ -0,0 +1,111 @@
+import mongoose from 'mongoose';
+
+import { IPageRedirect, PageRedirectModel } from '../../../src/server/models/page-redirect';
+import { getInstance } from '../setup-crowi';
+
+describe('PageRedirect', () => {
+  // eslint-disable-next-line no-unused-vars
+  let crowi;
+  let PageRedirect;
+
+  beforeAll(async() => {
+    crowi = await getInstance();
+
+    PageRedirect = mongoose.model('PageRedirect');
+  });
+
+  beforeEach(async() => {
+    // clear collection
+    await PageRedirect.deleteMany({});
+  });
+
+  describe('.removePageRedirectsByToPath', () => {
+    test('works fine', async() => {
+      // setup:
+      await PageRedirect.insertMany([
+        { fromPath: '/org/path1', toPath: '/path1' },
+        { fromPath: '/org/path2', toPath: '/path2' },
+        { fromPath: '/org/path3', toPath: '/path3' },
+        { fromPath: '/org/path33', toPath: '/org/path333' },
+        { fromPath: '/org/path333', toPath: '/path3' },
+      ]);
+      expect(await PageRedirect.findOne({ fromPath: '/org/path1' })).not.toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/org/path2' })).not.toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/org/path3' })).not.toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/org/path33' })).not.toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/org/path333' })).not.toBeNull();
+
+      // when:
+      // remove all documents that have { toPath: '/path/3' }
+      await PageRedirect.removePageRedirectsByToPath('/path3');
+
+      // then:
+      expect(await PageRedirect.findOne({ fromPath: '/org/path1' })).not.toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/org/path2' })).not.toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/org/path3' })).toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/org/path33' })).toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/org/path333' })).toBeNull();
+    });
+  });
+
+  describe('.retrievePageRedirectEndpoints', () => {
+    test('shoud return null when data is not found', async() => {
+      // setup:
+      expect(await PageRedirect.findOne({ fromPath: '/path1' })).toBeNull();
+
+      // when:
+      // retrieve
+      const endpoints = await PageRedirect.retrievePageRedirectEndpoints('/path1');
+
+      // then:
+      expect(endpoints).toBeNull();
+    });
+
+    test('shoud return IPageRedirectEnds (start and end is the same)', async() => {
+      // setup:
+      await PageRedirect.insertMany([
+        { fromPath: '/path1', toPath: '/path2' },
+      ]);
+      expect(await PageRedirect.findOne({ fromPath: '/path1' })).not.toBeNull();
+
+      // when:
+      // retrieve
+      const endpoints = await PageRedirect.retrievePageRedirectEndpoints('/path1');
+
+      // then:
+      expect(endpoints).not.toBeNull();
+      expect(endpoints.start).not.toBeNull();
+      expect(endpoints.start.fromPath).toEqual('/path1');
+      expect(endpoints.start.toPath).toEqual('/path2');
+      expect(endpoints.end).not.toBeNull();
+      expect(endpoints.end.fromPath).toEqual('/path1');
+      expect(endpoints.end.toPath).toEqual('/path2');
+    });
+
+    test('shoud return IPageRedirectEnds', async() => {
+      // setup:
+      await PageRedirect.insertMany([
+        { fromPath: '/path1', toPath: '/path2' },
+        { fromPath: '/path2', toPath: '/path3' },
+        { fromPath: '/path3', toPath: '/path4' },
+      ]);
+      expect(await PageRedirect.findOne({ fromPath: '/path1' })).not.toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/path2' })).not.toBeNull();
+      expect(await PageRedirect.findOne({ fromPath: '/path3' })).not.toBeNull();
+
+      // when:
+      // retrieve
+      const endpoints = await PageRedirect.retrievePageRedirectEndpoints('/path1');
+
+      // then:
+      expect(endpoints).not.toBeNull();
+      expect(endpoints.start).not.toBeNull();
+      expect(endpoints.start.fromPath).toEqual('/path1');
+      expect(endpoints.start.toPath).toEqual('/path2');
+      expect(endpoints.end).not.toBeNull();
+      expect(endpoints.end.fromPath).toEqual('/path3');
+      expect(endpoints.end.toPath).toEqual('/path4');
+    });
+  });
+
+});

+ 20 - 0
yarn.lock

@@ -19338,6 +19338,14 @@ source-map-support@0.5.19, source-map-support@^0.5.12:
     buffer-from "^1.0.0"
     source-map "^0.6.0"
 
+source-map-support@^0.5.17:
+  version "0.5.21"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+  integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
 source-map-support@^0.5.6:
   version "0.5.12"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599"
@@ -20865,6 +20873,18 @@ ts-node@^10.9.1:
     v8-compile-cache-lib "^3.0.1"
     yn "3.1.1"
 
+ts-node@^9.1.1:
+  version "9.1.1"
+  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d"
+  integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==
+  dependencies:
+    arg "^4.1.0"
+    create-require "^1.1.0"
+    diff "^4.0.1"
+    make-error "^1.1.1"
+    source-map-support "^0.5.17"
+    yn "3.1.1"
+
 tsc-alias@^1.2.9:
   version "1.2.9"
   resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.2.9.tgz#9fbf38e5eb1bd89c7f4fc26ef0712e22a6ef8939"