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

Merge branch 'support/apply-nextjs-2' into imprv/104022-draw-io-on-editor-navbar

kaori 3 лет назад
Родитель
Сommit
8d2b6e4815
53 измененных файлов с 825 добавлено и 468 удалено
  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. 19 19
      packages/app/package.json
  6. 23 9
      packages/app/public/static/locales/en_US/translation.json
  7. 17 3
      packages/app/public/static/locales/ja_JP/translation.json
  8. 17 3
      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. 101 55
      packages/app/src/components/LoginForm.tsx
  13. 70 46
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  14. 15 1
      packages/app/src/components/Navbar/GrowiSubNavigation.module.scss
  15. 18 40
      packages/app/src/components/Navbar/GrowiSubNavigation.tsx
  16. 11 6
      packages/app/src/components/Navbar/PersonalDropdown.jsx
  17. 1 2
      packages/app/src/components/PagePathNav.tsx
  18. 8 7
      packages/app/src/components/PasswordResetExecutionForm.tsx
  19. 13 12
      packages/app/src/components/PasswordResetRequestForm.tsx
  20. 4 3
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  21. 2 2
      packages/app/src/components/Skelton.tsx
  22. 7 0
      packages/app/src/interfaces/errors/forgot-password.ts
  23. 8 3
      packages/app/src/pages/[[...path]].page.tsx
  24. 90 0
      packages/app/src/pages/forgot-password-errors.page.tsx
  25. 60 0
      packages/app/src/pages/forgot-password.page.tsx
  26. 1 1
      packages/app/src/pages/me/[[...path]].page.tsx
  27. 72 0
      packages/app/src/pages/reset-password.page.tsx
  28. 1 1
      packages/app/src/pages/utils/commons.ts
  29. 15 2
      packages/app/src/server/middlewares/inject-reset-order-by-token-middleware.ts
  30. 9 7
      packages/app/src/server/middlewares/login-form-validator.ts
  31. 8 8
      packages/app/src/server/middlewares/register-form-validator.ts
  32. 5 0
      packages/app/src/server/routes/apiv3/index.js
  33. 41 14
      packages/app/src/server/routes/forgot-password.ts
  34. 4 4
      packages/app/src/server/routes/index.js
  35. 113 90
      packages/app/src/server/routes/login-passport.js
  36. 6 10
      packages/app/src/server/routes/login.js
  37. 3 1
      packages/app/src/server/routes/next.ts
  38. 2 2
      packages/app/src/stores/context.tsx
  39. 6 5
      packages/app/src/stores/ui.tsx
  40. 0 9
      packages/app/src/styles/_on-edit.scss
  41. 0 20
      packages/app/src/styles/atoms/_buttons.scss
  42. 2 0
      packages/app/src/styles/atoms/_custom_control.scss
  43. 0 13
      packages/app/src/styles/atoms/_nav.scss
  44. 0 4
      packages/app/src/styles/atoms/_pre.scss
  45. 2 5
      packages/app/src/styles/style-next.scss
  46. 1 1
      packages/codemirror-textlint/package.json
  47. 1 1
      packages/core/package.json
  48. 1 1
      packages/plugin-attachment-refs/package.json
  49. 6 11
      packages/plugin-lsx/package.json
  50. 1 1
      packages/remark-growi-plugin/package.json
  51. 1 1
      packages/slack/package.json
  52. 2 2
      packages/slackbot-proxy/package.json
  53. 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)
 
 

+ 19 - 19
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",
@@ -150,6 +150,7 @@
     "prom-client": "^13.0.0",
     "prom-client": "^13.0.0",
     "rate-limiter-flexible": "^2.3.7",
     "rate-limiter-flexible": "^2.3.7",
     "react": "^18.2.0",
     "react": "^18.2.0",
+    "react-bootstrap-typeahead": "^5.2.2",
     "react-card-flip": "^1.0.10",
     "react-card-flip": "^1.0.10",
     "react-datepicker": "^4.7.0",
     "react-datepicker": "^4.7.0",
     "react-dnd": "^14.0.5",
     "react-dnd": "^14.0.5",
@@ -159,6 +160,9 @@
     "react-markdown": "^8.0.3",
     "react-markdown": "^8.0.3",
     "react-multiline-clamp": "^2.0.0",
     "react-multiline-clamp": "^2.0.0",
     "react-syntax-highlighter": "^15.5.0",
     "react-syntax-highlighter": "^15.5.0",
+    "react-use-ripple": "^1.5.2",
+    "react-scroll": "^1.8.7",
+    "reactstrap": "^8.9.0",
     "reconnecting-websocket": "^4.4.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "redis": "^3.0.2",
     "rehype-katex": "^6.0.2",
     "rehype-katex": "^6.0.2",
@@ -172,12 +176,17 @@
     "remark-math": "^5.1.1",
     "remark-math": "^5.1.1",
     "remark-wiki-link": "^1.0.4",
     "remark-wiki-link": "^1.0.4",
     "rimraf": "^3.0.0",
     "rimraf": "^3.0.0",
+    "simplebar-react": "^2.3.6",
     "socket.io": "^4.2.0",
     "socket.io": "^4.2.0",
+    "sticky-events": "^3.4.11",
     "stream-to-promise": "^3.0.0",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
     "superjson": "^1.9.1",
     "swagger-jsdoc": "^6.1.0",
     "swagger-jsdoc": "^6.1.0",
     "swig-templates": "^2.0.2",
     "swig-templates": "^2.0.2",
+    "swr": "^1.3.0",
+    "throttle-debounce": "^3.0.1",
+    "toastr": "^2.1.2",
     "uglifycss": "^0.0.29",
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",
     "universal-bunyan": "^0.9.2",
     "unzipper": "^0.10.5",
     "unzipper": "^0.10.5",
@@ -185,7 +194,8 @@
     "usehooks-ts": "^2.6.0",
     "usehooks-ts": "^2.6.0",
     "validator": "^13.7.0",
     "validator": "^13.7.0",
     "ws": "^8.3.0",
     "ws": "^8.3.0",
-    "xss": "^1.0.6"
+    "xss": "^1.0.6",
+    "unstated": "^2.1.1"
   },
   },
   "// comments for defDependencies": {
   "// comments for defDependencies": {
     "@handsontable/react": "v3 requires handsontable >= 7.0.0.",
     "@handsontable/react": "v3 requires handsontable >= 7.0.0.",
@@ -193,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",
@@ -231,31 +241,21 @@
     "penpal": "^4.0.0",
     "penpal": "^4.0.0",
     "plantuml-encoder": "^1.2.5",
     "plantuml-encoder": "^1.2.5",
     "prettier": "^1.19.1",
     "prettier": "^1.19.1",
-    "react-bootstrap-typeahead": "^5.2.2",
     "react-codemirror2": "^6.0.0",
     "react-codemirror2": "^6.0.0",
     "react-copy-to-clipboard": "^5.0.1",
     "react-copy-to-clipboard": "^5.0.1",
     "react-dropzone": "^11.2.4",
     "react-dropzone": "^11.2.4",
     "react-frame-component": "^4.0.0",
     "react-frame-component": "^4.0.0",
     "react-hotkeys": "^2.0.0",
     "react-hotkeys": "^2.0.0",
-    "react-scroll": "^1.8.7",
-    "react-use-ripple": "^1.5.2",
     "react-waypoint": "^10.1.0",
     "react-waypoint": "^10.1.0",
-    "reactstrap": "^8.9.0",
     "rehype-rewrite": "^3.0.6",
     "rehype-rewrite": "^3.0.6",
     "replacestream": "^4.0.3",
     "replacestream": "^4.0.3",
     "reveal.js": "^4.3.1",
     "reveal.js": "^4.3.1",
     "sass": "^1.53.0",
     "sass": "^1.53.0",
     "simple-line-icons": "^2.5.5",
     "simple-line-icons": "^2.5.5",
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",
-    "simplebar-react": "^2.3.6",
     "socket.io-client": "^4.2.0",
     "socket.io-client": "^4.2.0",
-    "sticky-events": "^3.4.11",
     "swagger2openapi": "^5.3.1",
     "swagger2openapi": "^5.3.1",
-    "swr": "^1.3.0",
-    "throttle-debounce": "^3.0.1",
-    "toastr": "^2.1.2",
     "ts-node-dev": "^2.0.0",
     "ts-node-dev": "^2.0.0",
-    "tsc-alias": "^1.2.9",
-    "unstated": "^2.1.1"
+    "tsc-alias": "^1.2.9"
   }
   }
 }
 }

+ 23 - 9
packages/app/public/static/locales/en_US/translation.json

@@ -691,9 +691,19 @@
     "fail_to_save_access_token": "Failed to save access_token. Please try again.",
     "fail_to_save_access_token": "Failed to save access_token. Please try again.",
     "fail_to_fetch_access_token": "Failed to fetch access_token. Please do connect again.",
     "fail_to_fetch_access_token": "Failed to fetch access_token. Please do connect again.",
     "successfully_disconnected": "Successfully Disconnected!",
     "successfully_disconnected": "Successfully Disconnected!",
-    "strategy_has_not_been_set_up": "{{strategy}} has not been set up",
+    "strategy_has_not_been_set_up": {
+      "LocalStrategy"  : "LocalStrategy has not been set up",
+      "LdapStrategy"   : "LdapStrategy has not been set up",
+      "GoogleStrategy" : "GoogleStrategy has not been set up",
+      "GitHubStrategy" : "GitHubStrategy has not been set up",
+      "TwitterStrategy": "TwitterStrategy has not been set up",
+      "OidcStrategy"   : "OidcStrategy has not been set up",
+      "SamlStrategy"   : "SamlStrategy has not been set up",
+      "Basic"          : "Basic has not been set up"
+    },
+    "ldap_user_not_valid": "Ldap user is no valid",
+    "external_account_not_exist": "Failed to find or create External account",
     "maximum_number_of_users": "Can not register more than the maximum number of users.",
     "maximum_number_of_users": "Can not register more than the maximum number of users.",
-    "database_error": "Database Server Error occured",
     "sign_in_failure": "Sign in failure.",
     "sign_in_failure": "Sign in failure.",
     "aws_sttings_required": "AWS settings required to use this function. Please ask the administrator.",
     "aws_sttings_required": "AWS settings required to use this function. Please ask the administrator.",
     "application_already_installed": "Application already installed.",
     "application_already_installed": "Application already installed.",
@@ -715,13 +725,16 @@
     "user_already_loggedin": "You cannot create a new account when you are logged in.",
     "user_already_loggedin": "You cannot create a new account when you are logged in.",
     "registration_closed": "You are not authorized to create a new account.",
     "registration_closed": "You are not authorized to create a new account.",
     "Username has invalid characters": "Username has invalid characters.",
     "Username has invalid characters": "Username has invalid characters.",
-    "Username field is required": "User ID field is required",
-    "Name field is required": "Name field is required",
-    "Email format is invalid": "Email format is invalid",
-    "Email field is required": "Email field is required",
-    "Password has invalid character": "Password has invalid character",
-    "Password minimum character should be more than 8 characters": "Password minimum character should be more than 8 characters",
-    "Password field is required": "Password field is required"
+    "Username field is required": "User ID field is required.",
+    "Name field is required": "Name field is required.",
+    "Email format is invalid": "Email format is invalid.",
+    "Email field is required": "Email field is required.",
+    "Password has invalid character": "Password has invalid character.",
+    "Password minimum character should be more than 8 characters": "Password minimum character should be more than 8 characters.",
+    "Password field is required": "Password field is required.",
+    "Username or E-mail has invalid characters": "Username or E-mail has invalid characters.",
+    "Password minimum character should be more than 6 characters": "Password minimum character should be more than 6 characters.",
+    "user_not_found": "User not found."
   },
   },
   "grid_edit":{
   "grid_edit":{
     "create_bootstrap_4_grid":"Create Bootstrap 4 Grid",
     "create_bootstrap_4_grid":"Create Bootstrap 4 Grid",
@@ -748,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"
   },
   },

+ 17 - 3
packages/app/public/static/locales/ja_JP/translation.json

@@ -682,9 +682,19 @@
     "fail_to_save_access_token": "アクセストークンの保存に失敗しました、再度お試しください。",
     "fail_to_save_access_token": "アクセストークンの保存に失敗しました、再度お試しください。",
     "fail_to_fetch_access_token": "アクセストークンの取得に失敗しました、再度お試しください。",
     "fail_to_fetch_access_token": "アクセストークンの取得に失敗しました、再度お試しください。",
     "successfully_disconnected": "切断に成功しました!",
     "successfully_disconnected": "切断に成功しました!",
-    "strategy_has_not_been_set_up": "{{strategy}} はセットアップされていません。",
+    "strategy_has_not_been_set_up": {
+      "LocalStrategy"  : "LocalStrategy はセットアップされていません。",
+      "LdapStrategy"   : "LdapStrategy はセットアップされていません。",
+      "GoogleStrategy" : "GoogleStrategy はセットアップされていません。",
+      "GitHubStrategy" : "GitHubStrategy はセットアップされていません。",
+      "TwitterStrategy": "TwitterStrategy はセットアップされていません。",
+      "OidcStrategy"   : "OidcStrategy はセットアップされていません。",
+      "SamlStrategy"   : "SamlStrategy はセットアップされていません。",
+      "Basic"          : "Basic はセットアップされていません。"
+    },
+    "ldap_user_not_valid": "Ldap user is no valid",
+    "external_account_not_exist": "外部アカウントが見つからない、または作成に失敗しました",
     "maximum_number_of_users": "ユーザー数が上限を超えたためアクティベートできません。",
     "maximum_number_of_users": "ユーザー数が上限を超えたためアクティベートできません。",
-    "database_error":"データベースサーバーに問題があります。",
     "sign_in_failure": "ログインに失敗しました。",
     "sign_in_failure": "ログインに失敗しました。",
     "aws_sttings_required": "この機能にはAWS設定が必要です。管理者に訪ねて下さい。",
     "aws_sttings_required": "この機能にはAWS設定が必要です。管理者に訪ねて下さい。",
     "application_already_installed": "アプリケーションのインストールが完了しました。",
     "application_already_installed": "アプリケーションのインストールが完了しました。",
@@ -712,7 +722,10 @@
     "Email field is required": "メールアドレスは必須項目です",
     "Email field is required": "メールアドレスは必須項目です",
     "Password has invalid character": "パスワードに無効な文字があります",
     "Password has invalid character": "パスワードに無効な文字があります",
     "Password minimum character should be more than 8 characters": "パスワードの最小文字数は8文字以上です",
     "Password minimum character should be more than 8 characters": "パスワードの最小文字数は8文字以上です",
-    "Password field is required": "パスワードの欄は必ず入力してください"
+    "Password field is required": "パスワードの欄は必ず入力してください",
+    "Username or E-mail has invalid characters": "ユーザー名または、メールアドレスに無効な文字があります",
+    "Password minimum character should be more than 6 characters": "パスワードの最小文字数は6文字以上です",
+    "user_not_found": "ユーザーが見つかりません"
   },
   },
   "grid_edit":{
   "grid_edit":{
     "create_bootstrap_4_grid":"Bootstrap 4 グリッドを作成",
     "create_bootstrap_4_grid":"Bootstrap 4 グリッドを作成",
@@ -739,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": "パスワードと確認パスワードが一致しません"
   },
   },

+ 17 - 3
packages/app/public/static/locales/zh_CN/translation.json

@@ -738,9 +738,19 @@
 		"fail_to_save_access_token": "无法保存访问令牌。请再试一次。",
 		"fail_to_save_access_token": "无法保存访问令牌。请再试一次。",
 		"fail_to_fetch_access_token": "无法获取访问令牌。请重新连接。",
 		"fail_to_fetch_access_token": "无法获取访问令牌。请重新连接。",
 		"successfully_disconnected": "成功断开连接!",
 		"successfully_disconnected": "成功断开连接!",
-		"strategy_has_not_been_set_up": "{{strategy}尚未设置",
+    "strategy_has_not_been_set_up": {
+      "LocalStrategy"  : "LocalStrategy 尚未设置",
+      "LdapStrategy"   : "LdapStrategy 尚未设置",
+      "GoogleStrategy" : "GoogleStrategy 尚未设置",
+      "GitHubStrategy" : "GitHubStrategy 尚未设置",
+      "TwitterStrategy": "TwitterStrategy 尚未设置",
+      "OidcStrategy"   : "OidcStrategy 尚未设置",
+      "SamlStrategy"   : "SamlStrategy 尚未设置",
+      "Basic"          : "Basic 尚未设置"
+     },
+    "ldap_user_not_valid": "Ldap user is no valid",
+    "external_account_not_exist": "查找或创建外部账户失败",
 		"maximum_number_of_users": "注册的用户数不能超过最大值。",
 		"maximum_number_of_users": "注册的用户数不能超过最大值。",
-		"database_error": "发生数据库服务器错误",
 		"sign_in_failure": "登录失败。",
 		"sign_in_failure": "登录失败。",
 		"aws_sttings_required": "使用此功能所需的AWS设置。请询问管理员。",
 		"aws_sttings_required": "使用此功能所需的AWS设置。请询问管理员。",
 		"application_already_installed": "应用程序已安装。",
 		"application_already_installed": "应用程序已安装。",
@@ -768,7 +778,10 @@
     "Email field is required": "电子邮件字段是必需的",
     "Email field is required": "电子邮件字段是必需的",
     "Password has invalid character": "密码有无效字符",
     "Password has invalid character": "密码有无效字符",
     "Password minimum character should be more than 8 characters": "密码最小字符应超过8个字符",
     "Password minimum character should be more than 8 characters": "密码最小字符应超过8个字符",
-    "Password field is required": "密码字段是必需的"
+    "Password field is required": "密码字段是必需的",
+    "Username or E-mail has invalid characters": "用户名或电子邮件有无效的字符",
+    "Password minimum character should be more than 6 characters": "密码最小字符应超过6个字符",
+    "user_not_found": "未找到用户"
 	},
 	},
   "grid_edit":{
   "grid_edit":{
     "create_bootstrap_4_grid":"创建Bootstrap 4网格",
     "create_bootstrap_4_grid":"创建Bootstrap 4网格",
@@ -795,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

@@ -46,7 +46,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>

+ 101 - 55
packages/app/src/components/LoginForm.tsx

@@ -7,7 +7,6 @@ import { useRouter } from 'next/router';
 import ReactCardFlip from 'react-card-flip';
 import ReactCardFlip from 'react-card-flip';
 
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
-import { useCsrfToken } from '~/stores/context';
 
 
 type LoginFormProps = {
 type LoginFormProps = {
   username?: string,
   username?: string,
@@ -26,22 +25,25 @@ type LoginFormProps = {
 export const LoginForm = (props: LoginFormProps): JSX.Element => {
 export const LoginForm = (props: LoginFormProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const router = useRouter();
   const router = useRouter();
-  const { data: csrfToken } = useCsrfToken();
 
 
   const {
   const {
     isLocalStrategySetup, isLdapStrategySetup, isPasswordResetEnabled, isRegistrationEnabled,
     isLocalStrategySetup, isLdapStrategySetup, isPasswordResetEnabled, isRegistrationEnabled,
-    isEmailAuthenticationEnabled, registrationMode, registrationWhiteList, isMailerSetup,
+    isEmailAuthenticationEnabled, registrationMode, registrationWhiteList, isMailerSetup, objOfIsExternalAuthEnableds,
   } = props;
   } = props;
   const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
   const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
-  // const isSomeExternalAuthEnabled = Object.values(objOfIsExternalAuthEnableds).some(elem => elem);
-  const isSomeExternalAuthEnabled = true;
+  const isSomeExternalAuthEnabled = Object.values(objOfIsExternalAuthEnableds).some(elem => elem);
 
 
   // states
   // states
   const [isRegistering, setIsRegistering] = useState(false);
   const [isRegistering, setIsRegistering] = useState(false);
-  const [username, setUsername] = useState('');
-  const [name, setName] = useState('');
-  const [email, setEmail] = useState('');
-  const [password, setPassword] = useState('');
+  // For Login
+  const [usernameForLogin, setUsernameForLogin] = useState('');
+  const [passwordForLogin, setPasswordForLogin] = useState('');
+  const [loginErrors, setLoginErrors] = useState<Error[]>([]);
+  // For Register
+  const [usernameForRegister, setUsernameForRegister] = useState('');
+  const [nameForRegister, setNameForRegister] = useState('');
+  const [emailForRegister, setEmailForRegister] = useState('');
+  const [passwordForRegister, setPasswordForRegister] = useState('');
   const [registerErrors, setRegisterErrors] = useState<Error[]>([]);
   const [registerErrors, setRegisterErrors] = useState<Error[]>([]);
 
 
   useEffect(() => {
   useEffect(() => {
@@ -57,49 +59,86 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
 
     window.location.href = `/passport/${auth}`;
     window.location.href = `/passport/${auth}`;
   }, []);
   }, []);
+
+  const handleLoginWithLocalSubmit = useCallback(async(e) => {
+    e.preventDefault();
+
+    const loginForm = {
+      username: usernameForLogin,
+      password: passwordForLogin,
+    };
+
+    try {
+      const res = await apiv3Post('/login', { loginForm });
+      const { redirectTo } = res.data;
+      router.push(redirectTo);
+    }
+    catch (err) {
+      setLoginErrors(err);
+    }
+    return;
+
+  }, [passwordForLogin, router, usernameForLogin]);
+
   const renderLocalOrLdapLoginForm = useCallback(() => {
   const renderLocalOrLdapLoginForm = useCallback(() => {
     const { isLdapStrategySetup } = props;
     const { isLdapStrategySetup } = props;
-
     return (
     return (
-      <form role="form" action="/login" method="post">
-        <div className="input-group">
-          <div className="input-group-prepend">
-            <span className="input-group-text">
-              <i className="icon-user"></i>
-            </span>
-          </div>
-          <input type="text" className="form-control rounded-0" data-testid="tiUsernameForLogin" placeholder="Username or E-mail" name="loginForm[username]" />
-          {isLdapStrategySetup && (
-            <div className="input-group-append">
-              <small className="input-group-text text-success">
-                <i className="icon-fw icon-check"></i> LDAP
-              </small>
+      <>
+        {
+          loginErrors != null && loginErrors.length > 0 && (
+            <p className="alert alert-danger">
+              {loginErrors.map((err, index) => {
+                return (
+                  <span key={index}>
+                    {t(err.message)}<br/>
+                  </span>
+                );
+              })}
+            </p>
+          )
+        }
+        <form role="form" onSubmit={handleLoginWithLocalSubmit} id="login-form">
+          <div className="input-group">
+            <div className="input-group-prepend">
+              <span className="input-group-text">
+                <i className="icon-user"></i>
+              </span>
             </div>
             </div>
-          )}
-        </div>
+            <input type="text" className="form-control rounded-0" data-testid="tiUsernameForLogin" placeholder="Username or E-mail"
+              onChange={(e) => { setUsernameForLogin(e.target.value) }} name="usernameForLogin" />
+            {isLdapStrategySetup && (
+              <div className="input-group-append">
+                <small className="input-group-text text-success">
+                  <i className="icon-fw icon-check"></i> LDAP
+                </small>
+              </div>
+            )}
+          </div>
 
 
-        <div className="input-group">
-          <div className="input-group-prepend">
-            <span className="input-group-text">
-              <i className="icon-lock"></i>
-            </span>
+          <div className="input-group">
+            <div className="input-group-prepend">
+              <span className="input-group-text">
+                <i className="icon-lock"></i>
+              </span>
+            </div>
+            <input type="password" className="form-control rounded-0" data-testid="tiPasswordForLogin" placeholder="Password"
+              onChange={(e) => { setPasswordForLogin(e.target.value) }} name="passwordForLogin" />
           </div>
           </div>
-          <input type="password" className="form-control rounded-0" data-testid="tiPasswordForLogin" placeholder="Password" name="loginForm[password]" />
-        </div>
 
 
-        <div className="input-group my-4">
-          <input type="hidden" name="_csrf" value={csrfToken} />
-          <button type="submit" id="login" className="btn btn-fill rounded-0 login mx-auto" data-testid="btnSubmitForLogin">
-            <div className="eff"></div>
-            <span className="btn-label">
-              <i className="icon-login"></i>
-            </span>
-            <span className="btn-label-text">{t('Sign in')}</span>
-          </button>
-        </div>
-      </form>
+          <div className="input-group my-4">
+            <button type="submit" id="login" className="btn btn-fill rounded-0 login mx-auto" data-testid="btnSubmitForLogin">
+              <div className="eff"></div>
+              <span className="btn-label">
+                <i className="icon-login"></i>
+              </span>
+              <span className="btn-label-text">{t('Sign in')}</span>
+            </button>
+          </div>
+        </form>
+      </>
     );
     );
-  }, [csrfToken, props, t]);
+  }, [handleLoginWithLocalSubmit, loginErrors, props, t]);
+
   const renderExternalAuthInput = useCallback((auth) => {
   const renderExternalAuthInput = useCallback((auth) => {
     const authIconNames = {
     const authIconNames = {
       google: 'google',
       google: 'google',
@@ -124,6 +163,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       </div>
       </div>
     );
     );
   }, [handleLoginWithExternalAuth, t]);
   }, [handleLoginWithExternalAuth, t]);
+
   const renderExternalAuthLoginForm = useCallback(() => {
   const renderExternalAuthLoginForm = useCallback(() => {
     const { isLocalStrategySetup, isLdapStrategySetup, objOfIsExternalAuthEnableds } = props;
     const { isLocalStrategySetup, isLdapStrategySetup, objOfIsExternalAuthEnableds } = props;
     const isExternalAuthCollapsible = isLocalStrategySetup || isLdapStrategySetup;
     const isExternalAuthCollapsible = isLocalStrategySetup || isLdapStrategySetup;
@@ -163,10 +203,10 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     e.preventDefault();
     e.preventDefault();
 
 
     const registerForm = {
     const registerForm = {
-      username,
-      name,
-      email,
-      password,
+      username: usernameForRegister,
+      name: nameForRegister,
+      email: emailForRegister,
+      password: passwordForRegister,
     };
     };
     try {
     try {
       const res = await apiv3Post(requestPath, { registerForm });
       const res = await apiv3Post(requestPath, { registerForm });
@@ -180,7 +220,12 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       }
       }
     }
     }
     return;
     return;
-  }, [email, name, password, router, username]);
+  }, [emailForRegister, nameForRegister, passwordForRegister, router, usernameForRegister]);
+
+  const resetLoginErrors = useCallback(() => {
+    if (loginErrors.length === 0) return;
+    setLoginErrors([]);
+  }, [loginErrors.length]);
 
 
   const resetRegisterErrors = useCallback(() => {
   const resetRegisterErrors = useCallback(() => {
     if (registerErrors.length === 0) return;
     if (registerErrors.length === 0) return;
@@ -189,8 +234,9 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
 
   const switchForm = useCallback(() => {
   const switchForm = useCallback(() => {
     setIsRegistering(!isRegistering);
     setIsRegistering(!isRegistering);
+    resetLoginErrors();
     resetRegisterErrors();
     resetRegisterErrors();
-  }, [isRegistering, resetRegisterErrors]);
+  }, [isRegistering, resetLoginErrors, resetRegisterErrors]);
 
 
   const renderRegisterForm = useCallback(() => {
   const renderRegisterForm = useCallback(() => {
     let registerAction = '/register';
     let registerAction = '/register';
@@ -222,7 +268,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
               {registerErrors.map((err, index) => {
               {registerErrors.map((err, index) => {
                 return (
                 return (
                   <span key={index}>
                   <span key={index}>
-                    {t(`message.${err.message}`)}<br/>
+                    {t(err.message)}<br/>
                   </span>
                   </span>
                 );
                 );
               })}
               })}
@@ -244,7 +290,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
                 <input
                 <input
                   type="text"
                   type="text"
                   className="form-control rounded-0"
                   className="form-control rounded-0"
-                  onChange={(e) => { setUsername(e.target.value) }}
+                  onChange={(e) => { setUsernameForRegister(e.target.value) }}
                   placeholder={t('User ID')}
                   placeholder={t('User ID')}
                   name="username"
                   name="username"
                   defaultValue={props.username}
                   defaultValue={props.username}
@@ -263,7 +309,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
                 {/* name */}
                 {/* name */}
                 <input type="text"
                 <input type="text"
                   className="form-control rounded-0"
                   className="form-control rounded-0"
-                  onChange={(e) => { setName(e.target.value) }}
+                  onChange={(e) => { setNameForRegister(e.target.value) }}
                   placeholder={t('Name')}
                   placeholder={t('Name')}
                   name="name"
                   name="name"
                   defaultValue={props.name}
                   defaultValue={props.name}
@@ -281,7 +327,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             {/* email */}
             {/* email */}
             <input type="email"
             <input type="email"
               className="form-control rounded-0"
               className="form-control rounded-0"
-              onChange={(e) => { setEmail(e.target.value) }}
+              onChange={(e) => { setEmailForRegister(e.target.value) }}
               placeholder={t('Email')}
               placeholder={t('Email')}
               name="email"
               name="email"
               defaultValue={props.email}
               defaultValue={props.email}
@@ -315,7 +361,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
                 {/* Password */}
                 {/* Password */}
                 <input type="password"
                 <input type="password"
                   className="form-control rounded-0"
                   className="form-control rounded-0"
-                  onChange={(e) => { setPassword(e.target.value) }}
+                  onChange={(e) => { setPasswordForRegister(e.target.value) }}
                   placeholder={t('Password')}
                   placeholder={t('Password')}
                   name="password"
                   name="password"
                   required />
                   required />

+ 70 - 46
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -1,6 +1,6 @@
 import React, { useState, useEffect, useCallback } from 'react';
 import React, { useState, useEffect, useCallback } from 'react';
 
 
-import { isPopulated } from '@growi/core';
+import { isPopulated, IUser } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
@@ -9,13 +9,13 @@ import { exportAsMarkdown } from '~/client/services/page-operation';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiPost } from '~/client/util/apiv1-client';
 import {
 import {
-  IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity, IPageHasId,
+  IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
 import { IResTagsUpdateApiv1 } from '~/interfaces/tag';
 import { IResTagsUpdateApiv1 } from '~/interfaces/tag';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
 import {
-  useCurrentPageId,
-  useCurrentPathname, useIsNotFound,
+  useCurrentPageId, useCurrentPathname,
+  useIsNotFound,
   useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId, useTemplateTagData,
   useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId, useTemplateTagData,
 } from '~/stores/context';
 } from '~/stores/context';
 import { usePageTagsForEditors } from '~/stores/editor';
 import { usePageTagsForEditors } from '~/stores/editor';
@@ -39,19 +39,27 @@ import { Skelton } from '../Skelton';
 import { GrowiSubNavigation } from './GrowiSubNavigation';
 import { GrowiSubNavigation } from './GrowiSubNavigation';
 import { SubNavButtonsProps } from './SubNavButtons';
 import { SubNavButtonsProps } from './SubNavButtons';
 
 
-
+import AuthorInfoStyles from './AuthorInfo.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 
 
 
 
+const AuthorInfoSkelton = () => <Skelton additionalClass={`${AuthorInfoStyles['grw-author-info-skelton']} py-1`} />;
+
+
 const PageEditorModeManager = dynamic(
 const PageEditorModeManager = dynamic(
   () => import('./PageEditorModeManager'),
   () => import('./PageEditorModeManager'),
   { ssr: false, loading: () => <Skelton additionalClass={`${PageEditorModeManagerStyles['grw-page-editor-mode-manager-skelton']}`} /> },
   { ssr: false, loading: () => <Skelton additionalClass={`${PageEditorModeManagerStyles['grw-page-editor-mode-manager-skelton']}`} /> },
 );
 );
+// TODO: If enable skeleton, we get hydration error when create a page from PageCreateModal
+// { ssr: false, loading: () => <Skelton additionalClass='btn-skelton py-2' /> },
 const SubNavButtons = dynamic<SubNavButtonsProps>(
 const SubNavButtons = dynamic<SubNavButtonsProps>(
   () => import('./SubNavButtons').then(mod => mod.SubNavButtons),
   () => import('./SubNavButtons').then(mod => mod.SubNavButtons),
-  { ssr: false, loading: () => <Skelton additionalClass='btn-skelton py-2' /> },
+  { ssr: false, loading: () => <></> },
 );
 );
-
+const AuthorInfo = dynamic(() => import('./AuthorInfo'), {
+  ssr: false,
+  loading: AuthorInfoSkelton,
+});
 
 
 type AdditionalMenuItemsProps = {
 type AdditionalMenuItemsProps = {
   pageId: string,
   pageId: string,
@@ -177,9 +185,9 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { data: pageId } = useCurrentPageId();
   const { data: pageId } = useCurrentPageId();
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
+  const { data: isNotFound } = useIsNotFound();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
-  const { data: isNotFound } = useIsNotFound();
   const { data: shareLinkId } = useShareLinkId();
   const { data: shareLinkId } = useShareLinkId();
 
 
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
@@ -295,7 +303,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   }, []);
   }, []);
 
 
 
 
-  const ControlComponents = useCallback(() => {
+  const RightComponent = useCallback(() => {
     const additionalMenuItemsRenderer = () => {
     const additionalMenuItemsRenderer = () => {
       if (revisionId == null || pageId == null) {
       if (revisionId == null || pageId == null) {
         return <></>;
         return <></>;
@@ -309,37 +317,56 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
         />
         />
       );
       );
     };
     };
+
     return (
     return (
       <>
       <>
-        <div className="d-flex flex-column align-items-end justify-content-center py-md-2" style={{ gap: `${isCompactMode ? '5px' : '7px'}` }}>
-
-          { isViewMode && (
-            <div className="h-50 w-100">
-              { pageId != null && (
-                <SubNavButtons
-                  isCompactMode={isCompactMode}
-                  pageId={pageId}
-                  revisionId={revisionId}
-                  shareLinkId={shareLinkId}
-                  path={path}
-                  disableSeenUserInfoPopover={isSharedUser}
-                  showPageControlDropdown={isAbleToShowPageManagement}
-                  additionalMenuItemRenderer={additionalMenuItemsRenderer}
-                  onClickDuplicateMenuItem={duplicateItemClickedHandler}
-                  onClickRenameMenuItem={renameItemClickedHandler}
-                  onClickDeleteMenuItem={deleteItemClickedHandler}
-                />
-              ) }
-            </div>
+        <div className="d-flex">
+          <div className="d-flex flex-column align-items-end justify-content-center py-md-2" style={{ gap: `${isCompactMode ? '5px' : '7px'}` }}>
+            { isViewMode && (
+              <div className="h-50 w-100">
+                { pageId != null && (
+                  <SubNavButtons
+                    isCompactMode={isCompactMode}
+                    pageId={pageId}
+                    revisionId={revisionId}
+                    shareLinkId={shareLinkId}
+                    path={path}
+                    disableSeenUserInfoPopover={isSharedUser}
+                    showPageControlDropdown={isAbleToShowPageManagement}
+                    additionalMenuItemRenderer={additionalMenuItemsRenderer}
+                    onClickDuplicateMenuItem={duplicateItemClickedHandler}
+                    onClickRenameMenuItem={renameItemClickedHandler}
+                    onClickDeleteMenuItem={deleteItemClickedHandler}
+                  />
+                ) }
+              </div>
+            ) }
+            {isAbleToShowPageEditorModeManager && (
+              <PageEditorModeManager
+                onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
+                isBtnDisabled={isGuestUser}
+                editorMode={editorMode}
+              />
+            )}
+          </div>
+          { (isAbleToShowPageAuthors && !isCompactMode) && (
+            <ul className={`${AuthorInfoStyles['grw-author-info']} text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3`}>
+              <li className="pb-1">
+                { currentPage != null
+                  ? <AuthorInfo user={currentPage.creator as IUser} date={currentPage.createdAt} locate="subnav" />
+                  : <AuthorInfoSkelton />
+                }
+              </li>
+              <li className="mt-1 pt-1 border-top">
+                { currentPage != null
+                  ? <AuthorInfo user={currentPage.lastUpdateUser as IUser} date={currentPage.updatedAt} mode="update" locate="subnav" />
+                  : <AuthorInfoSkelton />
+                }
+              </li>
+            </ul>
           ) }
           ) }
-          {isAbleToShowPageEditorModeManager && (
-            <PageEditorModeManager
-              onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
-              isBtnDisabled={isGuestUser}
-              editorMode={editorMode}
-            />
-          )}
         </div>
         </div>
+
         {path != null && currentUser != null && (
         {path != null && currentUser != null && (
           <CreateTemplateModal
           <CreateTemplateModal
             path={path}
             path={path}
@@ -350,28 +377,25 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       </>
       </>
     );
     );
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  }, [currentUser, pageId, revisionId, shareLinkId, path, editorMode, isCompactMode, isViewMode, isSharedUser, isAbleToShowPageManagement, isAbleToShowPageEditorModeManager, isLinkSharingDisabled, isGuestUser, isPageTemplateModalShown, duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, mutateEditorMode, templateMenuItemClickHandler]);
+  }, [isCompactMode, isViewMode, pageId, revisionId, shareLinkId, path, isSharedUser, isAbleToShowPageManagement, duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, isAbleToShowPageEditorModeManager, isGuestUser, editorMode, isAbleToShowPageAuthors, currentPage, currentUser, isPageTemplateModalShown, isLinkSharingDisabled, templateMenuItemClickHandler, mutateEditorMode]);
 
 
-  if (currentPathname == null) {
-    return <></>;
-  }
 
 
-  const notFoundPage: Partial<IPageHasId> = {
-    path: currentPathname,
-  };
+  const pagePath = isNotFound
+    ? currentPathname
+    : currentPage?.path;
 
 
   return (
   return (
     <GrowiSubNavigation
     <GrowiSubNavigation
-      page={currentPage ?? notFoundPage}
+      pagePath={pagePath}
+      pageId={currentPage?._id}
       showDrawerToggler={isDrawerMode}
       showDrawerToggler={isDrawerMode}
       showTagLabel={isAbleToShowTagLabel}
       showTagLabel={isAbleToShowTagLabel}
-      showPageAuthors={isAbleToShowPageAuthors}
       isGuestUser={isGuestUser}
       isGuestUser={isGuestUser}
       isDrawerMode={isDrawerMode}
       isDrawerMode={isDrawerMode}
       isCompactMode={isCompactMode}
       isCompactMode={isCompactMode}
       tags={isViewMode ? tagsInfoData?.tags : tagsForEditors}
       tags={isViewMode ? tagsInfoData?.tags : tagsForEditors}
       tagsUpdatedHandler={isViewMode ? tagsUpdatedHandlerForViewMode : tagsUpdatedHandlerForEditMode}
       tagsUpdatedHandler={isViewMode ? tagsUpdatedHandlerForViewMode : tagsUpdatedHandlerForEditMode}
-      controls={ControlComponents}
+      rightComponent={RightComponent}
       additionalClasses={['container-fluid']}
       additionalClasses={['container-fluid']}
     />
     />
   );
   );

+ 15 - 1
packages/app/src/components/Navbar/GrowiSubNavigation.module.scss

@@ -44,6 +44,21 @@
       }
       }
     }
     }
 
 
+    .btn-copy {
+      &:not(:hover):not(:active) {
+        background-color: transparent !important;
+      }
+      opacity: 0.5;
+    }
+
+    .btn-edit-tags {
+      opacity: 0.5;
+
+      &.no-tags {
+        opacity: 0.7;
+      }
+    }
+
     .btn-skelton {
     .btn-skelton {
       @extend %subnav-buttons-height;
       @extend %subnav-buttons-height;
       width: 100%;
       width: 100%;
@@ -109,7 +124,6 @@
   &:global {
   &:global {
     &:hover {
     &:hover {
       .btn-copy,
       .btn-copy,
-      .btn-edit,
       .btn-edit-tags {
       .btn-edit-tags {
         // change button opacity
         // change button opacity
         opacity: unset;
         opacity: unset;

+ 18 - 40
packages/app/src/components/Navbar/GrowiSubNavigation.tsx

@@ -2,42 +2,37 @@ import React from 'react';
 
 
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
-import { IPageHasId } from '~/interfaces/page';
-import { IUser } from '~/interfaces/user';
 import {
 import {
   EditorMode, useEditorMode,
   EditorMode, useEditorMode,
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
 import { TagLabelsSkelton } from '../Page/TagLabels';
 import { TagLabelsSkelton } from '../Page/TagLabels';
 import PagePathNav from '../PagePathNav';
 import PagePathNav from '../PagePathNav';
-import { Skelton } from '../Skelton';
 
 
 import DrawerToggler from './DrawerToggler';
 import DrawerToggler from './DrawerToggler';
 
 
-import AuthorInfoStyles from './AuthorInfo.module.scss';
+
 import styles from './GrowiSubNavigation.module.scss';
 import styles from './GrowiSubNavigation.module.scss';
 
 
+
 const TagLabels = dynamic(() => import('../Page/TagLabels').then(mod => mod.TagLabels), {
 const TagLabels = dynamic(() => import('../Page/TagLabels').then(mod => mod.TagLabels), {
   ssr: false,
   ssr: false,
-  loading: () => <TagLabelsSkelton />,
-});
-const AuthorInfo = dynamic(() => import('./AuthorInfo'), {
-  ssr: false,
-  loading: () => <Skelton additionalClass={`${AuthorInfoStyles['grw-author-info-skelton']} py-1`} />,
+  loading: TagLabelsSkelton,
 });
 });
 
 
 
 
 export type GrowiSubNavigationProps = {
 export type GrowiSubNavigationProps = {
-  page: Partial<IPageHasId>,
+  pagePath?: string,
+  pageId?: string,
+  isNotFound?: boolean,
   showDrawerToggler?: boolean,
   showDrawerToggler?: boolean,
   showTagLabel?: boolean,
   showTagLabel?: boolean,
-  showPageAuthors?: boolean,
   isGuestUser?: boolean,
   isGuestUser?: boolean,
   isDrawerMode?: boolean,
   isDrawerMode?: boolean,
   isCompactMode?: boolean,
   isCompactMode?: boolean,
   tags?: string[],
   tags?: string[],
   tagsUpdatedHandler?: (newTags: string[]) => Promise<void> | void,
   tagsUpdatedHandler?: (newTags: string[]) => Promise<void> | void,
-  controls: React.FunctionComponent,
+  rightComponent: React.FunctionComponent,
   additionalClasses?: string[],
   additionalClasses?: string[],
 }
 }
 
 
@@ -46,11 +41,11 @@ export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element
   const { data: editorMode } = useEditorMode();
   const { data: editorMode } = useEditorMode();
 
 
   const {
   const {
-    page,
-    showDrawerToggler, showTagLabel, showPageAuthors,
+    pageId, pagePath,
+    showDrawerToggler, showTagLabel,
     isGuestUser, isDrawerMode, isCompactMode,
     isGuestUser, isDrawerMode, isCompactMode,
     tags, tagsUpdatedHandler,
     tags, tagsUpdatedHandler,
-    controls: Controls,
+    rightComponent: RightComponent,
     additionalClasses = [],
     additionalClasses = [],
   } = props;
   } = props;
 
 
@@ -58,15 +53,6 @@ export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element
   const isEditorMode = !isViewMode;
   const isEditorMode = !isViewMode;
   const compactModeClasses = isCompactMode ? 'grw-subnav-compact d-print-none' : '';
   const compactModeClasses = isCompactMode ? 'grw-subnav-compact d-print-none' : '';
 
 
-  const {
-    _id: pageId, path, creator, lastUpdateUser,
-    createdAt, updatedAt,
-  } = page;
-
-  if (path == null) {
-    return <></>;
-  }
-
   return (
   return (
     <div className={`grw-subnav ${styles['grw-subnav']} d-flex align-items-center justify-content-between ${additionalClasses.join(' ')}
     <div className={`grw-subnav ${styles['grw-subnav']} d-flex align-items-center justify-content-between ${additionalClasses.join(' ')}
     ${compactModeClasses}`} >
     ${compactModeClasses}`} >
@@ -80,27 +66,19 @@ export const GrowiSubNavigation = (props: GrowiSubNavigationProps): JSX.Element
         <div className="grw-path-nav-container">
         <div className="grw-path-nav-container">
           { (showTagLabel && !isCompactMode) && (
           { (showTagLabel && !isCompactMode) && (
             <div className="grw-taglabels-container">
             <div className="grw-taglabels-container">
-              <TagLabels tags={tags} isGuestUser={isGuestUser ?? false} tagsUpdateInvoked={tagsUpdatedHandler} />
+              { tags != null
+                ? <TagLabels tags={tags} isGuestUser={isGuestUser ?? false} tagsUpdateInvoked={tagsUpdatedHandler} />
+                : <TagLabelsSkelton />
+              }
             </div>
             </div>
           ) }
           ) }
-          <PagePathNav pageId={pageId} pagePath={path} isSingleLineMode={isEditorMode} isCompactMode={isCompactMode} />
+          { pagePath != null && (
+            <PagePathNav pageId={pageId} pagePath={pagePath} isSingleLineMode={isEditorMode} isCompactMode={isCompactMode} />
+          ) }
         </div>
         </div>
       </div>
       </div>
       {/* Right side. */}
       {/* Right side. */}
-      <div className="d-flex">
-        <Controls />
-        {/* Page Authors */}
-        { (showPageAuthors && !isCompactMode) && (
-          <ul className={`${AuthorInfoStyles['grw-author-info']} text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3`}>
-            <li className="pb-1">
-              <AuthorInfo user={creator as IUser} date={createdAt} locate="subnav" />
-            </li>
-            <li className="mt-1 pt-1 border-top">
-              <AuthorInfo user={lastUpdateUser as IUser} date={updatedAt} mode="update" locate="subnav" />
-            </li>
-          </ul>
-        ) }
-      </div>
+      <RightComponent />
     </div>
     </div>
   );
   );
 };
 };

+ 11 - 6
packages/app/src/components/Navbar/PersonalDropdown.jsx

@@ -2,6 +2,7 @@ import React, { useRef } from 'react';
 
 
 import { UserPicture } from '@growi/ui';
 import { UserPicture } from '@growi/ui';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
 import { useRipple } from 'react-use-ripple';
 import { useRipple } from 'react-use-ripple';
 
 
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
@@ -53,12 +54,16 @@ const PersonalDropdown = () => {
           </div>
           </div>
 
 
           <div className="btn-group btn-block mt-2" role="group">
           <div className="btn-group btn-block mt-2" role="group">
-            <a className="btn btn-sm btn-outline-secondary col" href={`/user/${user.username}`}>
-              <i className="icon-fw icon-home"></i>{ t('personal_dropdown.home') }
-            </a>
-            <a className="btn btn-sm btn-outline-secondary col" href="/me">
-              <i className="icon-fw icon-wrench"></i>{ t('personal_dropdown.settings') }
-            </a>
+            <Link href={`/user/${user.username}`}>
+              <a className="btn btn-sm btn-outline-secondary col">
+                <i className="icon-fw icon-home"></i>{ t('personal_dropdown.home') }
+              </a>
+            </Link>
+            <Link href="/me">
+              <a className="btn btn-sm btn-outline-secondary col">
+                <i className="icon-fw icon-wrench"></i>{ t('personal_dropdown.settings') }
+              </a>
+            </Link>
           </div>
           </div>
         </div>
         </div>
 
 

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

@@ -7,8 +7,6 @@ import { useIsNotFound } from '~/stores/context';
 
 
 import LinkedPagePath from '../models/linked-page-path';
 import LinkedPagePath from '../models/linked-page-path';
 
 
-import PagePathHierarchicalLink from './PagePathHierarchicalLink';
-
 const { isTrashPage } = pagePathUtils;
 const { isTrashPage } = pagePathUtils;
 
 
 type Props = {
 type Props = {
@@ -19,6 +17,7 @@ type Props = {
 }
 }
 
 
 const CopyDropdown = dynamic(() => import('./Page/CopyDropdown'), { ssr: false });
 const CopyDropdown = dynamic(() => import('./Page/CopyDropdown'), { ssr: false });
+const PagePathHierarchicalLink = dynamic(() => import('./PagePathHierarchicalLink'), { ssr: false });
 
 
 const PagePathNav: FC<Props> = (props: Props) => {
 const PagePathNav: FC<Props> = (props: Props) => {
   const {
   const {

+ 8 - 7
packages/app/src/components/PasswordResetExecutionForm.jsx → packages/app/src/components/PasswordResetExecutionForm.tsx

@@ -1,6 +1,7 @@
-import React, { useState } from 'react';
+import React, { FC, useState } 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 { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
@@ -9,7 +10,7 @@ import loggerFactory from '~/utils/logger';
 const logger = loggerFactory('growi:passwordReset');
 const logger = loggerFactory('growi:passwordReset');
 
 
 
 
-const PasswordResetExecutionForm = (props) => {
+const PasswordResetExecutionForm: FC = () => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const [newPassword, setNewPassword] = useState('');
   const [newPassword, setNewPassword] = useState('');
@@ -79,14 +80,14 @@ const PasswordResetExecutionForm = (props) => {
       <div className="form-group">
       <div className="form-group">
         <input name="reset-password-btn" className="btn btn-lg btn-primary btn-block" value={t('forgot_password.reset_password')} type="submit" />
         <input name="reset-password-btn" className="btn btn-lg btn-primary btn-block" value={t('forgot_password.reset_password')} type="submit" />
       </div>
       </div>
-      <a href="/login">
-        <i className="icon-login mr-1"></i>{t('forgot_password.sign_in_instead')}
-      </a>
+      <Link href="/login" prefetch={false}>
+        <a>
+          <i className="icon-login mr-1"></i>{t('forgot_password.sign_in_instead')}
+        </a>
+      </Link>
     </form>
     </form>
   );
   );
 };
 };
 
 
-PasswordResetExecutionForm.propTypes = {
-};
 
 
 export default PasswordResetExecutionForm;
 export default PasswordResetExecutionForm;

+ 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;

+ 4 - 3
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -170,7 +170,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
   }, [onDeletedHandler, openDeleteModal]);
   }, [onDeletedHandler, openDeleteModal]);
 
 
-  const ControlComponents = useCallback(() => {
+  const RightComponent = useCallback(() => {
     if (page == null) {
     if (page == null) {
       return <></>;
       return <></>;
     }
     }
@@ -202,8 +202,9 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     <div key={page._id} data-testid="search-result-content" className="search-result-content grw-page-path-text-muted-container d-flex flex-column">
     <div key={page._id} data-testid="search-result-content" className="search-result-content grw-page-path-text-muted-container d-flex flex-column">
       <div className="grw-subnav-append-shadow-container">
       <div className="grw-subnav-append-shadow-container">
         <GrowiSubNavigation
         <GrowiSubNavigation
-          page={page}
-          controls={ControlComponents}
+          pagePath={page.path}
+          pageId={page._id}
+          rightComponent={RightComponent}
           isCompactMode
           isCompactMode
           additionalClasses={['px-4']}
           additionalClasses={['px-4']}
         />
         />

+ 2 - 2
packages/app/src/components/Skelton.tsx

@@ -11,8 +11,8 @@ export const Skelton = (props: SkeltonProps): JSX.Element => {
   } = props;
   } = props;
 
 
   return (
   return (
-    <div className={`${additionalClass}`}>
-      <div className={`grw-skelton h-100 w-100 ${roundedPill ? 'rounded-pill' : ''}`}></div>
+    <div className={`${additionalClass ?? ''}`}>
+      <div className={`grw-skelton h-100 w-100 ${roundedPill ?? ''}`}></div>
     </div>
     </div>
   );
   );
 };
 };

+ 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]

+ 8 - 3
packages/app/src/pages/[[...path]].page.tsx

@@ -237,16 +237,16 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   }
   }
 
 
   const pageId = pageWithMeta?.data._id;
   const pageId = pageWithMeta?.data._id;
-  const pagePath = pageWithMeta?.data.path ?? props.currentPathname;
+  const pagePath = pageWithMeta?.data.path ?? (!_isPermalink(props.currentPathname) ? props.currentPathname : undefined);
 
 
   useCurrentPageId(pageId);
   useCurrentPageId(pageId);
   useSWRxCurrentPage(undefined, pageWithMeta?.data); // store initial data
   useSWRxCurrentPage(undefined, pageWithMeta?.data); // store initial data
-  useIsUserPage(isUserPage(pagePath));
+  useIsUserPage(pagePath != null && isUserPage(pagePath));
   // useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical
   // useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical
   useCurrentPagePath(pagePath);
   useCurrentPagePath(pagePath);
   useCurrentPathname(props.currentPathname);
   useCurrentPathname(props.currentPathname);
   useEditingMarkdown(pageWithMeta?.data.revision?.body);
   useEditingMarkdown(pageWithMeta?.data.revision?.body);
-  useIsTrashPage(_isTrashPage(pagePath));
+  useIsTrashPage(pagePath != null && _isTrashPage(pagePath));
 
 
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
@@ -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 || errorCode === 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;

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

@@ -196,7 +196,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
 
   await injectUserUISettings(context, props);
   await injectUserUISettings(context, props);
   await injectServerConfigurations(context, props);
   await injectServerConfigurations(context, props);
-  await injectNextI18NextConfigurations(context, props, ['translation']);
+  await injectNextI18NextConfigurations(context, props, ['translation', 'admin']);
 
 
   return {
   return {
     props,
     props,

+ 72 - 0
packages/app/src/pages/reset-password.page.tsx

@@ -0,0 +1,72 @@
+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';
+
+
+type Props = CommonProps & {
+  email: string
+};
+
+const PasswordResetExecutionForm = dynamic(() => import('~/components/PasswordResetExecutionForm'), { ssr: false });
+
+const ForgotPasswordPage: NextPage<Props> = (props: Props) => {
+  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-open large"></i></h1>
+                <h2 className="text-center">{ t('forgot_password.reset_password') }</h2>
+                <h5>{ props.email }</h5>
+                <p className="mt-4">{ t('forgot_password.password_reset_excecution_desc') }</p>
+                <PasswordResetExecutionForm />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+// eslint-disable-next-line max-len
+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 email = context.query.email;
+  if (typeof email === 'string') {
+    props.email = email;
+  }
+
+  await injectNextI18NextConfigurations(context, props, ['translation']);
+
+  return {
+    props,
+  };
+};
+
+export default ForgotPasswordPage;

+ 1 - 1
packages/app/src/pages/utils/commons.ts

@@ -44,7 +44,7 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     namespacesRequired: ['translation'],
     namespacesRequired: ['translation'],
     currentPathname,
     currentPathname,
     appTitle: appService.getAppTitle(),
     appTitle: appService.getAppTitle(),
-    siteUrl: appService.getSiteUrl(),
+    siteUrl: configManager.getConfig('crowi', 'app:siteUrl'), // DON'T USE appService.getSiteUrl()
     confidential: appService.getAppConfidential() || '',
     confidential: appService.getAppConfidential() || '',
     theme: configManager.getConfig('crowi', 'customize:theme'),
     theme: configManager.getConfig('crowi', 'customize:theme'),
     customTitleTemplate: customizeService.customTitleTemplate,
     customTitleTemplate: customizeService.customTitleTemplate,

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

@@ -1,24 +1,37 @@
 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 loggerFactory from '~/utils/logger';
+
 import PasswordResetOrder, { IPasswordResetOrder } from '../models/password-reset-order';
 import PasswordResetOrder, { IPasswordResetOrder } from '../models/password-reset-order';
 
 
+const logger = loggerFactory('growi:routes:forgot-password');
+
 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' }));
+    logger.error('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' }));
+    const message = 'passwordResetOrder is null or expired or revoked';
+    logger.error(message);
+    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;

+ 9 - 7
packages/app/src/server/middlewares/login-form-validator.ts

@@ -48,18 +48,18 @@ export const loginRules = () => {
   return [
   return [
     body('loginForm.username')
     body('loginForm.username')
       .matches(/^[\da-zA-Z\-_.+@]+$/)
       .matches(/^[\da-zA-Z\-_.+@]+$/)
-      .withMessage('Username or E-mail has invalid characters')
+      .withMessage('message.Username or E-mail has invalid characters')
       .not()
       .not()
       .isEmpty()
       .isEmpty()
-      .withMessage('Username field is required'),
+      .withMessage('message.Username field is required'),
     body('loginForm.password')
     body('loginForm.password')
       .matches(/^[\x20-\x7F]*$/)
       .matches(/^[\x20-\x7F]*$/)
-      .withMessage('Password has invalid character')
+      .withMessage('message.Password has invalid character')
       .isLength({ min: 6 })
       .isLength({ min: 6 })
-      .withMessage('Password minimum character should be more than 6 characters')
+      .withMessage('message.Password minimum character should be more than 6 characters')
       .not()
       .not()
       .isEmpty()
       .isEmpty()
-      .withMessage('Password field is required'),
+      .withMessage('message.Password field is required'),
   ];
   ];
 };
 };
 
 
@@ -77,8 +77,10 @@ export const loginValidation = (req, res, next) => {
   const extractedErrors: string[] = [];
   const extractedErrors: string[] = [];
   errors.array().map(err => extractedErrors.push(err.msg));
   errors.array().map(err => extractedErrors.push(err.msg));
 
 
-  req.flash('errorMessages', extractedErrors);
-  Object.assign(form, { isValid: false });
+  Object.assign(form, {
+    isValid: false,
+    errors: extractedErrors,
+  });
   req.form = form;
   req.form = form;
 
 
   return next();
   return next();

+ 8 - 8
packages/app/src/server/middlewares/register-form-validator.ts

@@ -6,24 +6,24 @@ export const registerRules = () => {
   return [
   return [
     body('registerForm.username')
     body('registerForm.username')
       .matches(/^[\da-zA-Z\-_.]+$/)
       .matches(/^[\da-zA-Z\-_.]+$/)
-      .withMessage('Username has invalid characters')
+      .withMessage('message.Username has invalid characters')
       .not()
       .not()
       .isEmpty()
       .isEmpty()
-      .withMessage('Username field is required'),
-    body('registerForm.name').not().isEmpty().withMessage('Name field is required'),
+      .withMessage('message.Username field is required'),
+    body('registerForm.name').not().isEmpty().withMessage('message.Name field is required'),
     body('registerForm.email')
     body('registerForm.email')
       .isEmail()
       .isEmail()
-      .withMessage('Email format is invalid')
+      .withMessage('message.Email format is invalid')
       .exists()
       .exists()
-      .withMessage('Email field is required'),
+      .withMessage('message.Email field is required'),
     body('registerForm.password')
     body('registerForm.password')
       .matches(/^[\x20-\x7F]*$/)
       .matches(/^[\x20-\x7F]*$/)
-      .withMessage('Password has invalid character')
+      .withMessage('message.Password has invalid character')
       .isLength({ min: PASSOWRD_MINIMUM_NUMBER })
       .isLength({ min: PASSOWRD_MINIMUM_NUMBER })
-      .withMessage('Password minimum character should be more than 8 characters')
+      .withMessage('message.Password minimum character should be more than 8 characters')
       .not()
       .not()
       .isEmpty()
       .isEmpty()
-      .withMessage('Password field is required'),
+      .withMessage('message.Password field is required'),
     body('registerForm[app:globalLang]'),
     body('registerForm[app:globalLang]'),
   ];
   ];
 };
 };

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

@@ -2,6 +2,7 @@ import loggerFactory from '~/utils/logger';
 
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import injectUserRegistrationOrderByTokenMiddleware from '../../middlewares/inject-user-registration-order-by-token-middleware';
 import injectUserRegistrationOrderByTokenMiddleware from '../../middlewares/inject-user-registration-order-by-token-middleware';
+import * as loginFormValidator from '../../middlewares/login-form-validator';
 import * as registerFormValidator from '../../middlewares/register-form-validator';
 import * as registerFormValidator from '../../middlewares/register-form-validator';
 
 
 import pageListing from './page-listing';
 import pageListing from './page-listing';
@@ -43,6 +44,10 @@ module.exports = (crowi, app, isInstalled) => {
   const applicationInstalled = require('../../middlewares/application-installed')(crowi);
   const applicationInstalled = require('../../middlewares/application-installed')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
   const login = require('../login')(crowi, app);
   const login = require('../login')(crowi, app);
+  const loginPassport = require('../login-passport')(crowi, app);
+
+  routerForAuth.post('/login', applicationInstalled, loginFormValidator.loginRules(), loginFormValidator.loginValidation,
+    addActivity, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.cannotLoginErrorHadnler, loginPassport.loginFailure);
 
 
   routerForAuth.use('/logout', require('./logout')(crowi));
   routerForAuth.use('/logout', require('./logout')(crowi));
 
 

+ 41 - 14
packages/app/src/server/routes/forgot-password.ts

@@ -1,12 +1,12 @@
 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 { IPasswordResetOrder } from '../models/password-reset-order';
 
 
 const logger = loggerFactory('growi:routes:forgot-password');
 const logger = loggerFactory('growi:routes:forgot-password');
 
 
@@ -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();
@@ -33,19 +33,46 @@ export const checkForgotPasswordEnabledMiddlewareFactory = (crowi: any, forApi =
 
 
 };
 };
 
 
-export const forgotPassword = (req: Request, res: Response): void => {
-  return res.render('forgot-password');
+type Crowi = {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  nextApp: any,
+}
+
+type CrowiReq = Request & {
+  crowi: Crowi,
+}
+
+export const renderForgotPassword = (crowi: Crowi) => {
+  return (req: CrowiReq, res: Response, next: NextFunction): void => {
+    const { nextApp } = crowi;
+    req.crowi = crowi;
+    nextApp.render(req, res, '/forgot-password');
+    return;
+  };
 };
 };
 
 
-export const resetPassword = (req: ReqWithPasswordResetOrder, res: Response): void => {
-  const { passwordResetOrder } = req;
-  return res.render('reset-password', { email: passwordResetOrder.email });
+export const renderResetPassword = (crowi: Crowi) => {
+  return (req: CrowiReq & { passwordResetOrder: IPasswordResetOrder }, res: Response, next: NextFunction): void => {
+    const { nextApp } = crowi;
+    req.crowi = crowi;
+    nextApp.render(req, res, '/reset-password', { email: req.passwordResetOrder.email });
+    return;
+  };
 };
 };
 
 
 // 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();
+  };
 };
 };

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

@@ -82,7 +82,6 @@ module.exports = function(crowi, app) {
   app.get('/login'                    , applicationInstalled, login.preLogin, next.delegateToNext);
   app.get('/login'                    , applicationInstalled, login.preLogin, next.delegateToNext);
   app.get('/invited'                  , applicationInstalled, next.delegateToNext);
   app.get('/invited'                  , applicationInstalled, next.delegateToNext);
   app.post('/invited/activateInvited' , applicationInstalled, loginFormValidator.inviteRules(), loginFormValidator.inviteValidation, csrfProtection, login.invited);
   app.post('/invited/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);
 
 
   app.get('/register'                 , applicationInstalled, login.preLogin, login.register);
   app.get('/register'                 , applicationInstalled, login.preLogin, login.register);
 
 
@@ -204,6 +203,7 @@ module.exports = function(crowi, app) {
   // app.get('/tags'                     , loginRequired, tag.showPage);
   // app.get('/tags'                     , loginRequired, tag.showPage);
   app.get('/tags', loginRequired, next.delegateToNext);
   app.get('/tags', loginRequired, next.delegateToNext);
 
 
+  app.get('/me'                                 , loginRequiredStrictly, injectUserUISettings, next.delegateToNext);
   app.get('/me/*'                                 , loginRequiredStrictly, injectUserUISettings, next.delegateToNext);
   app.get('/me/*'                                 , loginRequiredStrictly, injectUserUISettings, next.delegateToNext);
   // external-accounts
   // external-accounts
   // my in-app-notifications
   // my in-app-notifications
@@ -232,9 +232,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('/', forgotPassword.renderForgotPassword(crowi))
+    .get('/:token', injectResetOrderByTokenMiddleware, forgotPassword.renderResetPassword(crowi))
+    .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()

+ 113 - 90
packages/app/src/server/routes/login-passport.js

@@ -2,6 +2,8 @@ import { SupportedAction } from '~/interfaces/activity';
 import { NullUsernameToBeRegisteredError } from '~/server/models/errors';
 import { NullUsernameToBeRegisteredError } from '~/server/models/errors';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import ErrorV3 from '../models/vo/error-apiv3';
+
 /* eslint-disable no-use-before-define */
 /* eslint-disable no-use-before-define */
 
 
 module.exports = function(crowi, app) {
 module.exports = function(crowi, app) {
@@ -15,6 +17,74 @@ module.exports = function(crowi, app) {
 
 
   const ApiResponse = require('../util/apiResponse');
   const ApiResponse = require('../util/apiResponse');
 
 
+  const promisifiedPassportAuthentication = (strategyName, req, res) => {
+    return new Promise((resolve, reject) => {
+      passport.authenticate(strategyName, (err, response, info) => {
+        if (res.headersSent) { // dirty hack -- 2017.09.25
+          return; //              cz: somehow passport.authenticate called twice when ECONNREFUSED error occurred
+        }
+
+        logger.debug(`--- authenticate with ${strategyName} strategy ---`);
+
+        if (err) {
+          logger.error(`'${strategyName}' passport authentication error: `, err);
+          reject(err);
+        }
+
+        logger.debug('response', response);
+        logger.debug('info', info);
+
+        // authentication failure
+        if (!response) {
+          reject(response);
+        }
+
+        resolve(response);
+      })(req, res);
+    });
+  };
+
+  const getOrCreateUser = async(req, res, userInfo, providerId) => {
+    // get option
+    const isSameUsernameTreatedAsIdenticalUser = crowi.passportService.isSameUsernameTreatedAsIdenticalUser(providerId);
+    const isSameEmailTreatedAsIdenticalUser = crowi.passportService.isSameEmailTreatedAsIdenticalUser(providerId);
+
+    try {
+      // find or register(create) user
+      const externalAccount = await ExternalAccount.findOrRegister(
+        providerId,
+        userInfo.id,
+        userInfo.username,
+        userInfo.name,
+        userInfo.email,
+        isSameUsernameTreatedAsIdenticalUser,
+        isSameEmailTreatedAsIdenticalUser,
+      );
+      return externalAccount;
+    }
+    catch (err) {
+      /* eslint-disable no-else-return */
+      if (err instanceof NullUsernameToBeRegisteredError) {
+        logger.error(err.message);
+        throw Error(err.message);
+      }
+      else if (err.name === 'DuplicatedUsernameException') {
+        if (isSameEmailTreatedAsIdenticalUser || isSameUsernameTreatedAsIdenticalUser) {
+          // associate to existing user
+          debug(`ExternalAccount '${userInfo.username}' will be created and bound to the exisiting User account`);
+          return ExternalAccount.associate(providerId, userInfo.id, err.user);
+        }
+        logger.error('provider-DuplicatedUsernameException', providerId);
+        throw Error(`provider-DuplicatedUsernameException: ${providerId}`);
+      }
+      else if (err.name === 'UserUpperLimitException') {
+        logger.error(err.message);
+        throw Error(err.message);
+      }
+      /* eslint-enable no-else-return */
+    }
+  };
+
   /**
   /**
    * success handler
    * success handler
    * @param {*} req
    * @param {*} req
@@ -44,7 +114,7 @@ module.exports = function(crowi, app) {
     };
     };
     await crowi.activityService.createActivity(parameters);
     await crowi.activityService.createActivity(parameters);
 
 
-    return res.safeRedirect(redirectTo);
+    return res.apiv3({ redirectTo });
   };
   };
 
 
   /**
   /**
@@ -61,13 +131,23 @@ module.exports = function(crowi, app) {
     return res.redirect('/login');
     return res.redirect('/login');
   };
   };
 
 
+  const cannotLoginErrorHadnler = (req, res, next) => {
+    // this is called when all login method is somehow failed without invoking 'return next(<any Error>)'
+    const err = res.locals.err != null ? res.locals.err : Error('message.sign_in_failure');
+    return next(err);
+  };
+
   /**
   /**
    * middleware for login failure
    * middleware for login failure
    * @param {*} req
    * @param {*} req
    * @param {*} res
    * @param {*} res
    */
    */
-  const loginFailure = (req, res) => {
-    return loginFailureHandler(req, res, req.t('message.sign_in_failure'));
+  const loginFailure = (error, req, res, next) => {
+
+    const parameters = { action: SupportedAction.ACTION_USER_LOGIN_FAILURE };
+    activityEvent.emit('update', res.locals.activity._id, parameters);
+
+    return res.apiv3Err(error, error.code);
   };
   };
 
 
   /**
   /**
@@ -95,13 +175,11 @@ module.exports = function(crowi, app) {
   const loginWithLdap = async(req, res, next) => {
   const loginWithLdap = async(req, res, next) => {
     if (!passportService.isLdapStrategySetup) {
     if (!passportService.isLdapStrategySetup) {
       debug('LdapStrategy has not been set up');
       debug('LdapStrategy has not been set up');
-      return next();
+      return res.apiv3Err('message.strategy_has_not_been_set_up.LdapStrategy', 405);
     }
     }
 
 
     if (!req.form.isValid) {
     if (!req.form.isValid) {
-      debug('invalid form');
-      return res.render('login', {
-      });
+      return next(req.form.errors);
     }
     }
 
 
     const providerId = 'ldap';
     const providerId = 'ldap';
@@ -113,12 +191,12 @@ module.exports = function(crowi, app) {
     }
     }
     catch (err) {
     catch (err) {
       debug(err.message);
       debug(err.message);
-      return next();
+      return next(err);
     }
     }
 
 
     // check groups for LDAP
     // check groups for LDAP
     if (!isValidLdapUserByGroupFilter(ldapAccountInfo)) {
     if (!isValidLdapUserByGroupFilter(ldapAccountInfo)) {
-      return next();
+      return next(ErrorV3('message.ldap_user_not_valid', 400));
     }
     }
 
 
     /*
     /*
@@ -141,16 +219,27 @@ module.exports = function(crowi, app) {
       email: mailToBeRegistered,
       email: mailToBeRegistered,
     };
     };
 
 
-    const externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
-    if (!externalAccount) {
-      return next();
+    let externalAccount;
+    try {
+      externalAccount = await getOrCreateUser(req, res, userInfo, providerId);
+    }
+    catch (error) {
+      return next(error);
+    }
+
+    // just in case the returned value is null or undefined
+    if (externalAccount == null) {
+      return next(Error('message.external_account_not_exist'));
     }
     }
 
 
     const user = await externalAccount.getPopulatedUser();
     const user = await externalAccount.getPopulatedUser();
 
 
     // login
     // login
     await req.logIn(user, (err) => {
     await req.logIn(user, (err) => {
-      if (err) { debug(err.message); return next() }
+      if (err) {
+        debug(err.message);
+        return next(err);
+      }
 
 
       return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_LDAP);
       return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_LDAP);
     });
     });
@@ -221,13 +310,11 @@ module.exports = function(crowi, app) {
   const loginWithLocal = (req, res, next) => {
   const loginWithLocal = (req, res, next) => {
     if (!passportService.isLocalStrategySetup) {
     if (!passportService.isLocalStrategySetup) {
       debug('LocalStrategy has not been set up');
       debug('LocalStrategy has not been set up');
-      req.flash('warningMessage', req.t('message.strategy_has_not_been_set_up', { strategy: 'LocalStrategy' }));
-      return next();
+      return res.apiv3Err('message.strategy_has_not_been_set_up.LocalStrategy', 405);
     }
     }
 
 
     if (!req.form.isValid) {
     if (!req.form.isValid) {
-      return res.render('login', {
-      });
+      return next(req.form.errors);
     }
     }
 
 
     passport.authenticate('local', (err, user, info) => {
     passport.authenticate('local', (err, user, info) => {
@@ -237,12 +324,16 @@ module.exports = function(crowi, app) {
 
 
       if (err) { // DB Error
       if (err) { // DB Error
         logger.error('Database Server Error: ', err);
         logger.error('Database Server Error: ', err);
-        req.flash('warningMessage', req.t('message.database_error'));
-        return next(); // pass and the flash message is displayed when all of authentications are failed.
+        return next(err);
+      }
+      if (!user) {
+        return next();
       }
       }
-      if (!user) { return next() }
       req.logIn(user, (err) => {
       req.logIn(user, (err) => {
-        if (err) { debug(err.message); return next() }
+        if (err) {
+          debug(err.message);
+          return next(err);
+        }
 
 
         return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_LOCAL);
         return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_LOCAL);
       });
       });
@@ -560,76 +651,8 @@ module.exports = function(crowi, app) {
     });
     });
   };
   };
 
 
-  const promisifiedPassportAuthentication = (strategyName, req, res) => {
-    return new Promise((resolve, reject) => {
-      passport.authenticate(strategyName, (err, response, info) => {
-        if (res.headersSent) { // dirty hack -- 2017.09.25
-          return; //              cz: somehow passport.authenticate called twice when ECONNREFUSED error occurred
-        }
-
-        logger.debug(`--- authenticate with ${strategyName} strategy ---`);
-
-        if (err) {
-          logger.error(`'${strategyName}' passport authentication error: `, err);
-          reject(err);
-        }
-
-        logger.debug('response', response);
-        logger.debug('info', info);
-
-        // authentication failure
-        if (!response) {
-          reject(response);
-        }
-
-        resolve(response);
-      })(req, res);
-    });
-  };
-
-  const getOrCreateUser = async(req, res, userInfo, providerId) => {
-    // get option
-    const isSameUsernameTreatedAsIdenticalUser = crowi.passportService.isSameUsernameTreatedAsIdenticalUser(providerId);
-    const isSameEmailTreatedAsIdenticalUser = crowi.passportService.isSameEmailTreatedAsIdenticalUser(providerId);
-
-    try {
-      // find or register(create) user
-      const externalAccount = await ExternalAccount.findOrRegister(
-        providerId,
-        userInfo.id,
-        userInfo.username,
-        userInfo.name,
-        userInfo.email,
-        isSameUsernameTreatedAsIdenticalUser,
-        isSameEmailTreatedAsIdenticalUser,
-      );
-      return externalAccount;
-    }
-    catch (err) {
-      /* eslint-disable no-else-return */
-      if (err instanceof NullUsernameToBeRegisteredError) {
-        req.flash('warningMessage', req.t(`message.${err.message}`));
-        return;
-      }
-      else if (err.name === 'DuplicatedUsernameException') {
-        if (isSameEmailTreatedAsIdenticalUser || isSameUsernameTreatedAsIdenticalUser) {
-          // associate to existing user
-          debug(`ExternalAccount '${userInfo.username}' will be created and bound to the exisiting User account`);
-          return ExternalAccount.associate(providerId, userInfo.id, err.user);
-        }
-
-        req.flash('provider-DuplicatedUsernameException', providerId);
-        return;
-      }
-      else if (err.name === 'UserUpperLimitException') {
-        req.flash('warningMessage', req.t('message.maximum_number_of_users'));
-        return;
-      }
-      /* eslint-enable no-else-return */
-    }
-  };
-
   return {
   return {
+    cannotLoginErrorHadnler,
     loginFailure,
     loginFailure,
     loginWithLdap,
     loginWithLdap,
     testLdapCredentials,
     testLdapCredentials,

+ 6 - 10
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;
 
 
@@ -102,14 +106,6 @@ module.exports = function(crowi, app) {
     next();
     next();
   };
   };
 
 
-  actions.login = function(req, res) {
-    if (req.form) {
-      debug(req.form.errors);
-    }
-
-    return res.render('login', {});
-  };
-
   actions.register = function(req, res) {
   actions.register = function(req, res) {
     if (req.user != null) {
     if (req.user != null) {
       return res.apiv3Err('user_already_logged_in', 403);
       return res.apiv3Err('user_already_logged_in', 403);
@@ -122,7 +118,7 @@ module.exports = function(crowi, app) {
 
 
     if (!req.form.isValid) {
     if (!req.form.isValid) {
       const errors = req.form.errors;
       const errors = req.form.errors;
-      return res.apiv3Err(errors, 401);
+      return res.apiv3Err(errors, 400);
     }
     }
 
 
     const registerForm = req.form.registerForm || {};
     const registerForm = req.form.registerForm || {};

+ 3 - 1
packages/app/src/server/routes/next.ts

@@ -12,7 +12,7 @@ type CrowiReq = Request & {
 }
 }
 
 
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-export default (crowi: Crowi) => {
+const delegator = (crowi: Crowi) => {
 
 
   const { nextApp } = crowi;
   const { nextApp } = crowi;
   const handle = nextApp.getRequestHandler();
   const handle = nextApp.getRequestHandler();
@@ -27,3 +27,5 @@ export default (crowi: Crowi) => {
   };
   };
 
 
 };
 };
+
+export default delegator;

+ 2 - 2
packages/app/src/stores/context.tsx

@@ -55,8 +55,8 @@ export const useCurrentPagePath = (initialData?: Nullable<string>): SWRResponse<
   return useStaticSWR<Nullable<string>, Error>('currentPagePath', initialData);
   return useStaticSWR<Nullable<string>, Error>('currentPagePath', initialData);
 };
 };
 
 
-export const useCurrentPathname = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useStaticSWR<Nullable<string>, Error>('currentPathname', initialData);
+export const useCurrentPathname = (initialData?: string): SWRResponse<string, Error> => {
+  return useStaticSWR('currentPathname', initialData);
 };
 };
 
 
 export const useCurrentPageId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
 export const useCurrentPageId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {

+ 6 - 5
packages/app/src/stores/ui.tsx

@@ -21,7 +21,7 @@ import { UpdateDescCountData } from '~/interfaces/websocket';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import {
 import {
-  useCurrentPageId, useCurrentPagePath, useIsEditable, useIsTrashPage, useIsUserPage, useIsGuestUser,
+  useCurrentPageId, useCurrentPagePath, useIsEditable, useIsTrashPage, useIsGuestUser,
   useIsSharedUser, useIsIdenticalPath, useCurrentUser, useIsNotFound, useShareLinkId,
   useIsSharedUser, useIsIdenticalPath, useCurrentUser, useIsNotFound, useShareLinkId,
 } from './context';
 } from './context';
 import { localStorageMiddleware } from './middlewares/sync-to-storage';
 import { localStorageMiddleware } from './middlewares/sync-to-storage';
@@ -458,14 +458,15 @@ export const useIsAbleToShowPageEditorModeManager = (): SWRResponse<boolean, Err
 export const useIsAbleToShowPageAuthors = (): SWRResponse<boolean, Error> => {
 export const useIsAbleToShowPageAuthors = (): SWRResponse<boolean, Error> => {
   const key = 'isAbleToShowPageAuthors';
   const key = 'isAbleToShowPageAuthors';
   const { data: pageId } = useCurrentPageId();
   const { data: pageId } = useCurrentPageId();
-  const { data: isUserPage } = useIsUserPage();
+  const { data: pagePath } = useCurrentPagePath();
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
 
 
-  const includesUndefined = [pageId, isUserPage, isNotFound].some(v => v === undefined);
+  const includesUndefined = [pageId, pagePath, isNotFound].some(v => v === undefined);
   const isPageExist = (pageId != null) && !isNotFound;
   const isPageExist = (pageId != null) && !isNotFound;
+  const isUsersTopPagePath = pagePath != null && isUsersTopPage(pagePath);
 
 
   return useSWRImmutable(
   return useSWRImmutable(
-    includesUndefined ? null : [key, pageId],
-    () => isPageExist && !isUserPage,
+    includesUndefined ? null : [key, pageId, pagePath, isNotFound],
+    () => isPageExist && !isUsersTopPagePath,
   );
   );
 };
 };

+ 0 - 9
packages/app/src/styles/_on-edit.scss

@@ -165,15 +165,6 @@ body.on-edit {
     }
     }
   }
   }
 
 
-  .nav:hover {
-    .btn-copy,
-    .btn-edit,
-    .btn-edit-tags {
-      // change button opacity
-      opacity: unset;
-    }
-  }
-
   .grw-copy-dropdown {
   .grw-copy-dropdown {
     .btn-copy {
     .btn-copy {
       padding: 3px !important; // overwrite padding
       padding: 3px !important; // overwrite padding

+ 0 - 20
packages/app/src/styles/atoms/_buttons.scss

@@ -1,26 +1,6 @@
 @use '../bootstrap/init' as bs;
 @use '../bootstrap/init' as bs;
 @use '../mixins';
 @use '../mixins';
 
 
-.btn-copy,
-.btn-edit {
-  &:not(:hover):not(:active) {
-    background-color: transparent !important;
-  }
-  opacity: 0.5;
-}
-
-.btn-edit-tags {
-  opacity: 0.5;
-
-  &.no-tags {
-    opacity: 0.7;
-  }
-}
-
-.rounded-pill-weak {
-  border-radius: 60px;
-}
-
 // fill button style
 // fill button style
 .btn.btn-fill {
 .btn.btn-fill {
   position: relative;
   position: relative;

+ 2 - 0
packages/app/src/styles/atoms/_custom_control.scss

@@ -1,3 +1,5 @@
+@use '../bootstrap/init' as *;
+
 .custom-checkbox .custom-control-label::before {
 .custom-checkbox .custom-control-label::before {
   border-radius: $border-radius !important;
   border-radius: $border-radius !important;
 }
 }

+ 0 - 13
packages/app/src/styles/atoms/_nav.scss

@@ -1,13 +0,0 @@
-.nav-tabs .grw-main-nav-item-left {
-  width: $grw-nav-main-left-tab-width;
-  text-align: center;
-
-  @include media-breakpoint-down(sm) {
-    width: $grw-nav-main-left-tab-width-mobile;
-  }
-
-  .nav-link {
-    padding-right: 0;
-    padding-left: 0;
-  }
-}

+ 0 - 4
packages/app/src/styles/atoms/_pre.scss

@@ -1,4 +0,0 @@
-pre {
-  padding: 0.5em;
-  border-radius: $border-radius;
-}

+ 2 - 5
packages/app/src/styles/style-next.scss

@@ -27,11 +27,8 @@
 
 
 // atoms
 // atoms
 @import 'atoms/buttons';
 @import 'atoms/buttons';
-// @import 'atoms/code';
-// @import 'atoms/nav';
-// @import 'atoms/pre';
-// @import 'atoms/spinners';
-// @import 'atoms/custom_control';
+@import 'atoms/spinners';
+@import 'atoms/custom_control';
 
 
 // // molecules
 // // molecules
 // @import 'molecules/copy-dropdown';
 // @import 'molecules/copy-dropdown';

+ 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",