2
0
Эх сурвалжийг харах

Merge branch 'support/apply-nextjs-2' into support/105164-Hackmd-error-handling

Yuken Tezuka 3 жил өмнө
parent
commit
4a8ec87e46
28 өөрчлөгдсөн 279 нэмэгдсэн , 94 устгасан
  1. 1 1
      lerna.json
  2. 1 1
      package.json
  3. 1 2
      packages/app/bin/github-actions/update-readme.sh
  4. 3 4
      packages/app/docker/README.md
  5. 7 7
      packages/app/package.json
  6. 1 0
      packages/app/public/static/locales/en_US/translation.json
  7. 1 0
      packages/app/public/static/locales/ja_JP/translation.json
  8. 1 0
      packages/app/public/static/locales/zh_CN/translation.json
  9. 26 25
      packages/app/src/components/Fab.tsx
  10. 1 1
      packages/app/src/components/Layout/BasicLayout.tsx
  11. 1 1
      packages/app/src/components/Layout/ShareLinkLayout.tsx
  12. 13 12
      packages/app/src/components/PasswordResetRequestForm.tsx
  13. 7 0
      packages/app/src/interfaces/errors/forgot-password.ts
  14. 5 0
      packages/app/src/pages/[[...path]].page.tsx
  15. 90 0
      packages/app/src/pages/forgot-password-errors.page.tsx
  16. 60 0
      packages/app/src/pages/forgot-password.page.tsx
  17. 9 2
      packages/app/src/server/middlewares/inject-reset-order-by-token-middleware.ts
  18. 26 8
      packages/app/src/server/routes/forgot-password.ts
  19. 3 3
      packages/app/src/server/routes/index.js
  20. 5 1
      packages/app/src/server/routes/login.js
  21. 1 1
      packages/codemirror-textlint/package.json
  22. 1 1
      packages/core/package.json
  23. 1 1
      packages/plugin-attachment-refs/package.json
  24. 6 11
      packages/plugin-lsx/package.json
  25. 1 1
      packages/remark-growi-plugin/package.json
  26. 1 1
      packages/slack/package.json
  27. 2 2
      packages/slackbot-proxy/package.json
  28. 4 8
      packages/ui/package.json

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "5.1.5-RC.0",
+  "version": "6.0.0-RC.1",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",

+ 1 - 2
packages/app/bin/github-actions/update-readme.sh

@@ -2,5 +2,4 @@
 
 
 cd docker
 cd docker
 
 
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.1\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/packages\/app\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.1-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/packages\/app\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}-nocdn\2\3${RELEASED_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`6\.0\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/packages\/app\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md

+ 3 - 4
packages/app/docker/README.md

@@ -10,10 +10,9 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 ------------------------------------------------
 
 
-* [`5.1.4`, `5.1`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.4/packages/app/docker/Dockerfile)
-* [`5.1.4-nocdn`, `5.1-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.4/packages/app/docker/Dockerfile)
-* [`5.0.11`, `5.0` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
-* [`5.0.11-nocdn`, `5.0-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
+* [`6.0.0`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.0/packages/app/docker/Dockerfile)
+* [`5.1.4`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.4/packages/app/docker/Dockerfile)
+* [`5.1.4-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.4/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 * [`4.5.23-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 * [`4.5.23-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 
 

+ 7 - 7
packages/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "5.1.5-RC.0",
+  "version": "6.0.0-RC.1",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -64,11 +64,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.1.5-RC.0",
-    "@growi/core": "^5.1.5-RC.0",
-    "@growi/plugin-attachment-refs": "^5.1.5-RC.0",
-    "@growi/plugin-lsx": "^5.1.5-RC.0",
-    "@growi/slack": "^5.1.5-RC.0",
+    "@growi/codemirror-textlint": "^6.0.0-RC.1",
+    "@growi/core": "^6.0.0-RC.1",
+    "@growi/plugin-attachment-refs": "^6.0.0-RC.1",
+    "@growi/plugin-lsx": "^6.0.0-RC.1",
+    "@growi/slack": "^6.0.0-RC.1",
     "@promster/express": "^7.0.2",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
     "@slack/events-api": "^3.0.0",
@@ -203,7 +203,7 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.1.5-RC.0",
+    "@growi/ui": "^6.0.0-RC.1",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",
     "@next/bundle-analyzer": "^12.2.3",

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

@@ -761,6 +761,7 @@
     "confirm_new_password": "Confirm the new password",
     "confirm_new_password": "Confirm the new password",
     "email_is_required": "Email is required",
     "email_is_required": "Email is required",
     "success_to_send_email": "Success to send email",
     "success_to_send_email": "Success to send email",
+    "feature_is_unavailable": "This feature is unavailable.",
     "incorrect_token_or_expired_url": "The token is incorrect or the URL has expired. Please resend a password reset request via the link below.",
     "incorrect_token_or_expired_url": "The token is incorrect or the URL has expired. Please resend a password reset request via the link below.",
     "password_and_confirm_password_does_not_match": "Password and confirm password does not match"
     "password_and_confirm_password_does_not_match": "Password and confirm password does not match"
   },
   },

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

@@ -752,6 +752,7 @@
     "confirm_new_password": "新しいパスワードの確認",
     "confirm_new_password": "新しいパスワードの確認",
     "email_is_required": "メールを入力してください",
     "email_is_required": "メールを入力してください",
     "success_to_send_email": "メールを送信しました",
     "success_to_send_email": "メールを送信しました",
+    "feature_is_unavailable": "この機能を利用することはできません。",
     "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。 以下のリンクからパスワードリセットリクエストを再送信してください。",
     "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。 以下のリンクからパスワードリセットリクエストを再送信してください。",
     "password_and_confirm_password_does_not_match": "パスワードと確認パスワードが一致しません"
     "password_and_confirm_password_does_not_match": "パスワードと確認パスワードが一致しません"
   },
   },

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

@@ -808,6 +808,7 @@
     "confirm_new_password": "确认新密码",
     "confirm_new_password": "确认新密码",
     "email_is_required": "电子邮件是必需的",
     "email_is_required": "电子邮件是必需的",
     "success_to_send_email": "我发了一封电子邮件",
     "success_to_send_email": "我发了一封电子邮件",
+    "feature_is_unavailable": "此功能不可用",
     "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。 请通过以下链接重新发送密码重置请求",
     "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。 请通过以下链接重新发送密码重置请求",
     "password_and_confirm_password_does_not_match": "密码和确认密码不匹配"
     "password_and_confirm_password_does_not_match": "密码和确认密码不匹配"
   },
   },

+ 26 - 25
packages/app/src/components/Fab.tsx

@@ -31,32 +31,33 @@ export const Fab = (): JSX.Element => {
   useRipple(createBtnRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
   useRipple(createBtnRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
 
 
   /*
   /*
-  * Comment out to prevent err >>> TypeError: Cannot read properties of null (reading 'bottom')
+  * TODO: Comment out to prevent err >>> TypeError: Cannot read properties of null (reading 'bottom')
+  *       We need add style={{ position: 'relative }} to child elements if disable StickyEvents. see: use grep = "<Fab".
   */
   */
-  const stickyChangeHandler = useCallback((event) => {
-    logger.debug('StickyEvents.CHANGE detected');
-
-    const newAnimateClasses = event.detail.isSticky ? 'animated fadeInUp faster' : 'animated fadeOut faster';
-    const newButtonClasses = event.detail.isSticky ? '' : 'disabled grw-pointer-events-none';
-
-    setAnimateClasses(newAnimateClasses);
-    setButtonClasses(newButtonClasses);
-  }, []);
-
-  // setup effect by sticky event
-  useEffect(() => {
-    // sticky
-    // See: https://github.com/ryanwalters/sticky-events
-    const stickyEvents = new StickyEvents({ stickySelector: '#grw-fav-sticky-trigger' });
-    const { stickySelector } = stickyEvents;
-    const elem = document.querySelector(stickySelector);
-    elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-
-    // return clean up handler
-    return () => {
-      elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-    };
-  }, [stickyChangeHandler]);
+  // const stickyChangeHandler = useCallback((event) => {
+  //   logger.debug('StickyEvents.CHANGE detected');
+
+  //   const newAnimateClasses = event.detail.isSticky ? 'animated fadeInUp faster' : 'animated fadeOut faster';
+  //   const newButtonClasses = event.detail.isSticky ? '' : 'disabled grw-pointer-events-none';
+
+  //   setAnimateClasses(newAnimateClasses);
+  //   setButtonClasses(newButtonClasses);
+  // }, []);
+
+  // // setup effect by sticky event
+  // useEffect(() => {
+  //   // sticky
+  //   // See: https://github.com/ryanwalters/sticky-events
+  //   const stickyEvents = new StickyEvents({ stickySelector: '#grw-fav-sticky-trigger' });
+  //   const { stickySelector } = stickyEvents;
+  //   const elem = document.querySelector(stickySelector);
+  //   elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
+
+  //   // return clean up handler
+  //   return () => {
+  //     elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
+  //   };
+  // }, [stickyChangeHandler]);
 
 
   if (currentPath == null) {
   if (currentPath == null) {
     return <></>;
     return <></>;

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

@@ -45,7 +45,7 @@ export const BasicLayout = ({
           <Sidebar />
           <Sidebar />
         </div>
         </div>
 
 
-        <div className="flex-fill mw-0">
+        <div className="flex-fill mw-0" style={{ position: 'relative' }}>
           {children}
           {children}
         </div>
         </div>
       </div>
       </div>

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

@@ -33,7 +33,7 @@ export const ShareLinkLayout = ({
       <GrowiNavbar />
       <GrowiNavbar />
 
 
       <div className="page-wrapper d-flex d-print-block">
       <div className="page-wrapper d-flex d-print-block">
-        <div className="flex-fill mw-0">
+        <div className="flex-fill mw-0" style={{ position: 'relative' }}>
           {children}
           {children}
         </div>
         </div>
       </div>
       </div>

+ 13 - 12
packages/app/src/components/PasswordResetRequestForm.jsx → packages/app/src/components/PasswordResetRequestForm.tsx

@@ -1,20 +1,21 @@
-import React, { useState } from 'react';
+import React, { FC, useState, useCallback } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
 
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
 
 
 
 
-const PasswordResetRequestForm = (props) => {
+const PasswordResetRequestForm: FC = () => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [email, setEmail] = useState('');
   const [email, setEmail] = useState('');
 
 
-  const changeEmail = (inputValue) => {
+  const changeEmail = useCallback((inputValue) => {
     setEmail(inputValue);
     setEmail(inputValue);
-  };
+  }, []);
 
 
-  const sendPasswordResetRequestMail = async(e) => {
+  const sendPasswordResetRequestMail = useCallback(async(e) => {
     e.preventDefault();
     e.preventDefault();
     if (email === '') {
     if (email === '') {
       toastError('err', t('forgot_password.email_is_required'));
       toastError('err', t('forgot_password.email_is_required'));
@@ -28,10 +29,11 @@ const PasswordResetRequestForm = (props) => {
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  };
+  }, [t, email]);
 
 
   return (
   return (
     <form onSubmit={sendPasswordResetRequestMail}>
     <form onSubmit={sendPasswordResetRequestMail}>
+      <h3>{ t('forgot_password.password_reset_request_desc') }</h3>
       <div className="form-group">
       <div className="form-group">
         <div className="input-group">
         <div className="input-group">
           <input name="email" placeholder="E-mail Address" className="form-control" type="email" onChange={e => changeEmail(e.target.value)} />
           <input name="email" placeholder="E-mail Address" className="form-control" type="email" onChange={e => changeEmail(e.target.value)} />
@@ -45,14 +47,13 @@ const PasswordResetRequestForm = (props) => {
           {t('forgot_password.send')}
           {t('forgot_password.send')}
         </button>
         </button>
       </div>
       </div>
-      <a href="/login">
-        <i className="icon-login mr-1"></i>{t('forgot_password.return_to_login')}
-      </a>
+      <Link href='/login' prefetch={false}>
+        <a>
+          <i className="icon-login mr-1" />{t('forgot_password.return_to_login')}
+        </a>
+      </Link>
     </form>
     </form>
   );
   );
 };
 };
 
 
-PasswordResetRequestForm.propTypes = {
-};
-
 export default PasswordResetRequestForm;
 export default PasswordResetRequestForm;

+ 7 - 0
packages/app/src/interfaces/errors/forgot-password.ts

@@ -0,0 +1,7 @@
+export const forgotPasswordErrorCode = {
+  PASSWORD_RESET_IS_UNAVAILABLE: 'password-reset-is-unavailable',
+  TOKEN_NOT_FOUND: 'token-not-found',
+  PASSWORD_RESET_ORDER_IS_NOT_APPROPRIATE: 'password-reset-order-is-not-appropriate',
+} as const;
+
+export type forgotPasswordErrorCode = typeof forgotPasswordErrorCode[keyof typeof forgotPasswordErrorCode]

+ 5 - 0
packages/app/src/pages/[[...path]].page.tsx

@@ -399,6 +399,11 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
   const pageWithMeta: IPageToShowRevisionWithMeta = await pageService.findPageAndMetaDataByViewer(pageId, currentPathname, user, true); // includeEmpty = true, isSharedPage = false
   const pageWithMeta: IPageToShowRevisionWithMeta = await pageService.findPageAndMetaDataByViewer(pageId, currentPathname, user, true); // includeEmpty = true, isSharedPage = false
   const page = pageWithMeta?.data as unknown as PageDocument;
   const page = pageWithMeta?.data as unknown as PageDocument;
 
 
+  // add user to seen users
+  if (page != null && user != null) {
+    await page.seen(user);
+  }
+
   // populate & check if the revision is latest
   // populate & check if the revision is latest
   if (page != null) {
   if (page != null) {
     page.initLatestRevisionField(revisionId);
     page.initLatestRevisionField(revisionId);

+ 90 - 0
packages/app/src/pages/forgot-password-errors.page.tsx

@@ -0,0 +1,90 @@
+import React from 'react';
+
+import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { useTranslation } from 'next-i18next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import Link from 'next/link';
+
+import { forgotPasswordErrorCode } from '~/interfaces/errors/forgot-password';
+
+import {
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps,
+} from './utils/commons';
+
+type Props = CommonProps & {
+  errorCode?: forgotPasswordErrorCode
+};
+
+const ForgotPasswordErrorsPage: NextPage<Props> = (props: Props) => {
+  const { t } = useTranslation();
+  const { errorCode } = props;
+
+  return (
+    <div id="main" className="main">
+      <div id="content-main" className="content-main container-lg">
+        <div className="container">
+          <div className="row justify-content-md-center">
+            <div className="col-md-6 mt-5">
+              <div className="text-center">
+                <h1><i className="icon-lock-open large"/></h1>
+                <h2 className="text-center">{ t('forgot_password.reset_password') }</h2>
+
+                { errorCode == null && (
+                  <h3 className="text-muted">errorCode Unknown</h3>
+                )}
+
+                { errorCode === forgotPasswordErrorCode.PASSWORD_RESET_IS_UNAVAILABLE && (
+                  <h3 className="text-muted">{ t('forgot_password.feature_is_unavailable') }</h3>
+                )}
+
+                { errorCode === (forgotPasswordErrorCode.PASSWORD_RESET_ORDER_IS_NOT_APPROPRIATE || forgotPasswordErrorCode.TOKEN_NOT_FOUND) && (
+                  <div>
+                    <div className="alert alert-warning mb-3">
+                      <h2>{ t('forgot_password.incorrect_token_or_expired_url') }</h2>
+                    </div>
+                    <Link href="/forgot-password" prefetch={false}>
+                      <a className="link-switch">
+                        <i className="icon-key"></i> { t('forgot_password.forgot_password') }
+                      </a>
+                    </Link>
+                  </div>
+                ) }
+
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+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 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;
+
+  const errorCode = context.query.errorCode;
+  if (typeof errorCode === 'string') {
+    props.errorCode = errorCode as forgotPasswordErrorCode;
+  }
+
+  await injectNextI18NextConfigurations(context, props, ['translation']);
+
+  return {
+    props,
+  };
+};
+
+export default ForgotPasswordErrorsPage;

+ 60 - 0
packages/app/src/pages/forgot-password.page.tsx

@@ -0,0 +1,60 @@
+import React from 'react';
+
+import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { useTranslation } from 'next-i18next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import dynamic from 'next/dynamic';
+
+import {
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps,
+} from './utils/commons';
+
+const PasswordResetRequestForm = dynamic(() => import('~/components/PasswordResetRequestForm'), { ssr: false });
+
+const ForgotPasswordPage: NextPage = () => {
+  const { t } = useTranslation();
+
+  return (
+    <div id="main" className="main">
+      <div id="content-main" className="content-main container-lg">
+        <div className="container">
+          <div className="row justify-content-md-center">
+            <div className="col-md-6 mt-5">
+              <div className="text-center">
+                <h1><i className="icon-lock large"></i></h1>
+                <h1 className="text-center">{ t('forgot_password.forgot_password') }</h1>
+                <PasswordResetRequestForm />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+// eslint-disable-next-line max-len
+async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: CommonProps, namespacesRequired?: string[] | undefined): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  props._nextI18Next = nextI18NextConfig._nextI18Next;
+}
+
+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: CommonProps = result.props as CommonProps;
+
+  await injectNextI18NextConfigurations(context, props, ['translation']);
+
+  return {
+    props,
+  };
+};
+
+export default ForgotPasswordPage;

+ 9 - 2
packages/app/src/server/middlewares/inject-reset-order-by-token-middleware.ts

@@ -1,24 +1,31 @@
 import { NextFunction, Request, Response } from 'express';
 import { NextFunction, Request, Response } from 'express';
 import createError from 'http-errors';
 import createError from 'http-errors';
 
 
+import { forgotPasswordErrorCode } from '~/interfaces/errors/forgot-password';
+
 import PasswordResetOrder, { IPasswordResetOrder } from '../models/password-reset-order';
 import PasswordResetOrder, { IPasswordResetOrder } from '../models/password-reset-order';
 
 
 export type ReqWithPasswordResetOrder = Request & {
 export type ReqWithPasswordResetOrder = Request & {
   passwordResetOrder: IPasswordResetOrder,
   passwordResetOrder: IPasswordResetOrder,
 };
 };
 
 
+// eslint-disable-next-line import/no-anonymous-default-export
 export default async(req: ReqWithPasswordResetOrder, res: Response, next: NextFunction): Promise<void> => {
 export default async(req: ReqWithPasswordResetOrder, res: Response, next: NextFunction): Promise<void> => {
   const token = req.params.token || req.body.token;
   const token = req.params.token || req.body.token;
 
 
   if (token == null) {
   if (token == null) {
-    return next(createError(400, 'Token not found', { code: 'token-not-found' }));
+    return next(createError(400, 'Token not found', { code: forgotPasswordErrorCode.TOKEN_NOT_FOUND }));
   }
   }
 
 
   const passwordResetOrder = await PasswordResetOrder.findOne({ token });
   const passwordResetOrder = await PasswordResetOrder.findOne({ token });
 
 
   // check if the token is valid
   // check if the token is valid
   if (passwordResetOrder == null || passwordResetOrder.isExpired() || passwordResetOrder.isRevoked) {
   if (passwordResetOrder == null || passwordResetOrder.isExpired() || passwordResetOrder.isRevoked) {
-    return next(createError(400, 'passwordResetOrder is null or expired or revoked', { code: 'password-reset-order-is-not-appropriate' }));
+    return next(createError(
+      400,
+      'passwordResetOrder is null or expired or revoked',
+      { code: forgotPasswordErrorCode.PASSWORD_RESET_ORDER_IS_NOT_APPROPRIATE },
+    ));
   }
   }
 
 
   req.passwordResetOrder = passwordResetOrder;
   req.passwordResetOrder = passwordResetOrder;

+ 26 - 8
packages/app/src/server/routes/forgot-password.ts

@@ -1,9 +1,9 @@
 import {
 import {
-  NextFunction, Request, RequestHandler, Response,
+  NextFunction, Request, Response,
 } from 'express';
 } from 'express';
-
 import createError from 'http-errors';
 import createError from 'http-errors';
 
 
+import { forgotPasswordErrorCode } from '~/interfaces/errors/forgot-password';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { ReqWithPasswordResetOrder } from '../middlewares/inject-reset-order-by-token-middleware';
 import { ReqWithPasswordResetOrder } from '../middlewares/inject-reset-order-by-token-middleware';
@@ -25,7 +25,7 @@ export const checkForgotPasswordEnabledMiddlewareFactory = (crowi: any, forApi =
       logger.error(message);
       logger.error(message);
 
 
       const statusCode = forApi ? 405 : 404;
       const statusCode = forApi ? 405 : 404;
-      return next(createError(statusCode, message, { code: 'password-reset-is-unavailable' }));
+      return next(createError(statusCode, message, { code: forgotPasswordErrorCode.PASSWORD_RESET_IS_UNAVAILABLE }));
     }
     }
 
 
     next();
     next();
@@ -42,10 +42,28 @@ export const resetPassword = (req: ReqWithPasswordResetOrder, res: Response): vo
   return res.render('reset-password', { email: passwordResetOrder.email });
   return res.render('reset-password', { email: passwordResetOrder.email });
 };
 };
 
 
+type Crowi = {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  nextApp: any,
+}
+
+type CrowiReq = Request & {
+  crowi: Crowi,
+}
+
 // middleware to handle error
 // middleware to handle error
-export const handleErrosMiddleware = (error: Error & { code: string }, req: Request, res: Response, next: NextFunction): Promise<RequestHandler> | void => {
-  if (error != null) {
-    return res.render('forgot-password/error', { key: error.code });
-  }
-  next(error);
+export const handleErrorsMiddleware = (crowi: Crowi) => {
+  return (error: Error & { code: string, statusCode: number }, req: CrowiReq, res: Response, next: NextFunction): void => {
+    if (error != null) {
+      const { nextApp } = crowi;
+
+      req.crowi = crowi;
+      res.status(error.statusCode);
+
+      nextApp.render(req, res, '/forgot-password-errors', { errorCode: error.code });
+      return;
+    }
+
+    next();
+  };
 };
 };

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

@@ -231,9 +231,9 @@ module.exports = function(crowi, app) {
 
 
   app.use('/forgot-password', express.Router()
   app.use('/forgot-password', express.Router()
     .use(forgotPassword.checkForgotPasswordEnabledMiddlewareFactory(crowi))
     .use(forgotPassword.checkForgotPasswordEnabledMiddlewareFactory(crowi))
-    .get('/', forgotPassword.forgotPassword)
-    .get('/:token', injectResetOrderByTokenMiddleware, forgotPassword.resetPassword)
-    .use(forgotPassword.handleErrosMiddleware));
+    .get('/forgot-password', next.delegateToNext)
+    .get('/:token', injectResetOrderByTokenMiddleware, forgotPassword.resetPassword, next.delegateToNext) // TODO: 104986
+    .use(forgotPassword.handleErrorsMiddleware(crowi)));
 
 
   app.get('/_private-legacy-pages', next.delegateToNext);
   app.get('/_private-legacy-pages', next.delegateToNext);
   app.use('/user-activation', express.Router()
   app.use('/user-activation', express.Router()

+ 5 - 1
packages/app/src/server/routes/login.js

@@ -29,7 +29,11 @@ module.exports = function(crowi, app) {
         });
         });
       }
       }
 
 
-      const { redirectTo } = req.session;
+
+      // userData.password cann't be empty but, prepare redirect because password property in User Model is optional
+      // https://github.com/weseek/growi/pull/6670
+      const redirectTo = userData.password ? req.session.redirectTo : '/me#password';
+
       // remove session.redirectTo
       // remove session.redirectTo
       delete req.session.redirectTo;
       delete req.session.redirectTo;
 
 

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/codemirror-textlint",
   "name": "@growi/codemirror-textlint",
-  "version": "5.1.5-RC.0",
+  "version": "6.0.0-RC.1",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",
   "scripts": {
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/core",
   "name": "@growi/core",
-  "version": "5.1.5-RC.0",
+  "version": "6.0.0-RC.1",
   "description": "GROWI Core Libraries",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 1 - 1
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/plugin-attachment-refs",
   "name": "@growi/plugin-attachment-refs",
-  "version": "5.1.5-RC.0",
+  "version": "6.0.0-RC.1",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 6 - 11
packages/plugin-lsx/package.json

@@ -1,12 +1,9 @@
 {
 {
   "name": "@growi/plugin-lsx",
   "name": "@growi/plugin-lsx",
-  "version": "5.1.5-RC.0",
+  "version": "6.0.0-RC.1",
   "description": "GROWI plugin to list pages",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "license": "MIT",
-  "keywords": [
-    "growi",
-    "growi-plugin"
-  ],
+  "keywords": ["growi", "growi-plugin"],
   "main": "dist/cjs/index.js",
   "main": "dist/cjs/index.js",
   "module": "dist/esm/index.js",
   "module": "dist/esm/index.js",
   "exports": {
   "exports": {
@@ -14,9 +11,7 @@
     "./services/renderer": "./dist/cjs/services/renderer/index.js",
     "./services/renderer": "./dist/cjs/services/renderer/index.js",
     "./server/routes": "./dist/cjs/server/routes/index.js"
     "./server/routes": "./dist/cjs/server/routes/index.js"
   },
   },
-  "files": [
-    "dist"
-  ],
+  "files": ["dist"],
   "scripts": {
   "scripts": {
     "build": "run-p build:*",
     "build": "run-p build:*",
     "build:cjs": "tsc -p tsconfig.build.cjs.json && tsc-alias -p tsconfig.build.cjs.json",
     "build:cjs": "tsc -p tsconfig.build.cjs.json && tsc-alias -p tsconfig.build.cjs.json",
@@ -28,9 +23,9 @@
     "test": ""
     "test": ""
   },
   },
   "dependencies": {
   "dependencies": {
-    "@growi/core": "^5.1.5-RC.0",
-    "@growi/remark-growi-plugin": "^5.1.5-RC.0",
-    "@growi/ui": "^5.1.5-RC.0"
+    "@growi/core": "^6.0.0-RC.1",
+    "@growi/remark-growi-plugin": "^6.0.0-RC.1",
+    "@growi/ui": "^6.0.0-RC.1"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",

+ 1 - 1
packages/remark-growi-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-growi-plugin",
   "name": "@growi/remark-growi-plugin",
-  "version": "5.1.5-RC.0",
+  "version": "6.0.0-RC.1",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slack",
   "name": "@growi/slack",
-  "version": "5.1.5-RC.0",
+  "version": "6.0.0-RC.1",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
   "typings": "dist/index.d.ts",

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slackbot-proxy",
   "name": "@growi/slackbot-proxy",
-  "version": "5.1.5-slackbot-proxy.0",
+  "version": "6.0.0-slackbot-proxy.1",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -26,7 +26,7 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^5.1.5-RC.0",
+    "@growi/slack": "^6.0.0-RC.1",
     "@slack/oauth": "^2.0.1",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",
     "@tsed/common": "^6.43.0",

+ 4 - 8
packages/ui/package.json

@@ -1,16 +1,12 @@
 {
 {
   "name": "@growi/ui",
   "name": "@growi/ui",
-  "version": "5.1.5-RC.0",
+  "version": "6.0.0-RC.1",
   "description": "GROWI UI Libraries",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "license": "MIT",
-  "keywords": [
-    "growi"
-  ],
+  "keywords": ["growi"],
   "main": "dist/cjs/index.js",
   "main": "dist/cjs/index.js",
   "module": "dist/esm/index.js",
   "module": "dist/esm/index.js",
-  "files": [
-    "dist"
-  ],
+  "files": ["dist"],
   "scripts": {
   "scripts": {
     "build": "run-p build:*",
     "build": "run-p build:*",
     "build:cjs": "tsc -p tsconfig.build.cjs.json && tsc-alias -p tsconfig.build.cjs.json",
     "build:cjs": "tsc -p tsconfig.build.cjs.json && tsc-alias -p tsconfig.build.cjs.json",
@@ -21,7 +17,7 @@
     "test": "jest --verbose"
     "test": "jest --verbose"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@growi/core": "^5.1.5-RC.0"
+    "@growi/core": "^6.0.0-RC.1"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",