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

Merge pull request #6680 from weseek/imprv/return-proper-login-error-msg

imprv: Return login error msg properly
Yohei Shiina 3 лет назад
Родитель
Сommit
0e36b8a51e
37 измененных файлов с 200 добавлено и 128 удалено
  1. 6 12
      packages/app/public/static/locales/en_US/translation.json
  2. 6 12
      packages/app/public/static/locales/ja_JP/translation.json
  3. 6 12
      packages/app/public/static/locales/zh_CN/translation.json
  4. 76 23
      packages/app/src/components/LoginForm.tsx
  5. 5 0
      packages/app/src/interfaces/errors/login-error.ts
  6. 3 0
      packages/app/src/interfaces/errors/v3-error.ts
  7. 10 2
      packages/app/src/pages/login.page.tsx
  8. 2 2
      packages/app/src/server/middlewares/apiv3-form-validator.ts
  9. 7 12
      packages/app/src/server/models/vo/error-v3.js
  10. 2 3
      packages/app/src/server/routes/apiv3/app-settings.js
  11. 2 2
      packages/app/src/server/routes/apiv3/attachment.js
  12. 2 2
      packages/app/src/server/routes/apiv3/customize-setting.js
  13. 1 1
      packages/app/src/server/routes/apiv3/forgot-password.js
  14. 2 1
      packages/app/src/server/routes/apiv3/healthcheck.js
  15. 1 2
      packages/app/src/server/routes/apiv3/import.js
  16. 1 1
      packages/app/src/server/routes/apiv3/installer.ts
  17. 2 2
      packages/app/src/server/routes/apiv3/markdown-setting.js
  18. 2 2
      packages/app/src/server/routes/apiv3/notification-setting.js
  19. 1 1
      packages/app/src/server/routes/apiv3/page-listing.ts
  20. 3 2
      packages/app/src/server/routes/apiv3/page.js
  21. 2 2
      packages/app/src/server/routes/apiv3/pages.js
  22. 2 1
      packages/app/src/server/routes/apiv3/response.js
  23. 3 2
      packages/app/src/server/routes/apiv3/revisions.js
  24. 2 2
      packages/app/src/server/routes/apiv3/search.js
  25. 2 2
      packages/app/src/server/routes/apiv3/security-setting.js
  26. 2 2
      packages/app/src/server/routes/apiv3/share-links.js
  27. 2 2
      packages/app/src/server/routes/apiv3/slack-integration-legacy-settings.js
  28. 1 2
      packages/app/src/server/routes/apiv3/slack-integration-settings.js
  29. 1 2
      packages/app/src/server/routes/apiv3/slack-integration.js
  30. 2 1
      packages/app/src/server/routes/apiv3/user-activation.ts
  31. 2 1
      packages/app/src/server/routes/apiv3/user-group-relation.js
  32. 2 1
      packages/app/src/server/routes/apiv3/user-group.js
  33. 2 3
      packages/app/src/server/routes/apiv3/user-ui-settings.ts
  34. 2 1
      packages/app/src/server/routes/apiv3/users.js
  35. 17 10
      packages/app/src/server/routes/login-passport.js
  36. 1 0
      packages/core/src/index.ts
  37. 15 0
      packages/core/src/models/vo/error-apiv3.ts

+ 6 - 12
packages/app/public/static/locales/en_US/translation.json

@@ -674,7 +674,9 @@
   "login": {
     "Sign in error": "Login error",
     "Registration successful": "Registration successful",
-    "Setup": "Setup"
+    "Setup": "Setup",
+    "enabled_ldap_has_configuration_problem":"LDAP is enabled but the configuration has something wrong.",
+    "set_env_var_for_logs": "(Please set the environment variables <code>DEBUG=crowi:service:PassportService</code> to get the logs)"
   },
   "invited": {
     "discription_heading": "Create Account",
@@ -691,16 +693,7 @@
     "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.",
     "successfully_disconnected": "Successfully Disconnected!",
-    "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"
-    },
+    "strategy_has_not_been_set_up": "{{strategy}} 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.",
@@ -734,7 +727,8 @@
     "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."
+    "user_not_found": "User not found.",
+    "provider_duplicated_username_exception": "<p><strong><i class='icon-fw icon-ban'></i>DuplicatedUsernameException occured</strong></p><p class='mb-0'> Your {{ failedProviderForDuplicatedUsernameException }} authentication was succeeded, but a new user could not be created. See the issue <a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
   },
   "grid_edit":{
     "create_bootstrap_4_grid":"Create Bootstrap 4 Grid",

+ 6 - 12
packages/app/public/static/locales/ja_JP/translation.json

@@ -665,7 +665,9 @@
   "login": {
     "Sign in error": "ログインエラー",
     "Registration successful": "登録完了",
-    "Setup": "セットアップ"
+    "Setup": "セットアップ",
+    "enabled_ldap_has_configuration_problem":"LDAPは有効ですが、設定に問題があります。",
+    "set_env_var_for_logs": "(ログを取得するためには、環境変数 <code>DEBUG=crowi:service:PassportService</code> を設定してください。)"
   },
   "invited": {
     "discription_heading": "アカウント作成",
@@ -682,16 +684,7 @@
     "fail_to_save_access_token": "アクセストークンの保存に失敗しました、再度お試しください。",
     "fail_to_fetch_access_token": "アクセストークンの取得に失敗しました、再度お試しください。",
     "successfully_disconnected": "切断に成功しました!",
-    "strategy_has_not_been_set_up": {
-      "LocalStrategy"  : "LocalStrategy はセットアップされていません。",
-      "LdapStrategy"   : "LdapStrategy はセットアップされていません。",
-      "GoogleStrategy" : "GoogleStrategy はセットアップされていません。",
-      "GitHubStrategy" : "GitHubStrategy はセットアップされていません。",
-      "TwitterStrategy": "TwitterStrategy はセットアップされていません。",
-      "OidcStrategy"   : "OidcStrategy はセットアップされていません。",
-      "SamlStrategy"   : "SamlStrategy はセットアップされていません。",
-      "Basic"          : "Basic はセットアップされていません。"
-    },
+    "strategy_has_not_been_set_up": "{{strategy}} はセットアップされていません。",
     "ldap_user_not_valid": "Ldap user is no valid",
     "external_account_not_exist": "外部アカウントが見つからない、または作成に失敗しました",
     "maximum_number_of_users": "ユーザー数が上限を超えたためアクティベートできません。",
@@ -725,7 +718,8 @@
     "Password field is required": "パスワードの欄は必ず入力してください",
     "Username or E-mail has invalid characters": "ユーザー名または、メールアドレスに無効な文字があります",
     "Password minimum character should be more than 6 characters": "パスワードの最小文字数は6文字以上です",
-    "user_not_found": "ユーザーが見つかりません"
+    "user_not_found": "ユーザーが見つかりません",
+    "provider_duplicated_username_exception": "<p><strong><i class='icon-fw icon-ban'></i>エラー: DuplicatedUsernameException</strong></p><p class='mb-0'> {{ failedProviderForDuplicatedUsernameException }} 認証は成功しましたが、新しいユーザーを作成できませんでした。詳しくは<a href='https://github.com/weseek/growi/issues/193'>こちら: #193</a>.</p>"
   },
   "grid_edit":{
     "create_bootstrap_4_grid":"Bootstrap 4 グリッドを作成",

+ 6 - 12
packages/app/public/static/locales/zh_CN/translation.json

@@ -721,7 +721,9 @@
 	"login": {
 		"Sign in error": "登录错误",
 		"Registration successful": "注册成功",
-		"Setup": "安装程序"
+		"Setup": "安装程序",
+    "enabled_ldap_has_configuration_problem":"启用了LDAP,但配置有问题。",
+    "set_env_var_for_logs": "(请设置环境变量 <code>DEBUG=crowi:service:PassportService</code> 以获得日志。)"
 	},
   "invited": {
     "discription_heading": "创建账户",
@@ -738,16 +740,7 @@
 		"fail_to_save_access_token": "无法保存访问令牌。请再试一次。",
 		"fail_to_fetch_access_token": "无法获取访问令牌。请重新连接。",
 		"successfully_disconnected": "成功断开连接!",
-    "strategy_has_not_been_set_up": {
-      "LocalStrategy"  : "LocalStrategy 尚未设置",
-      "LdapStrategy"   : "LdapStrategy 尚未设置",
-      "GoogleStrategy" : "GoogleStrategy 尚未设置",
-      "GitHubStrategy" : "GitHubStrategy 尚未设置",
-      "TwitterStrategy": "TwitterStrategy 尚未设置",
-      "OidcStrategy"   : "OidcStrategy 尚未设置",
-      "SamlStrategy"   : "SamlStrategy 尚未设置",
-      "Basic"          : "Basic 尚未设置"
-     },
+    "strategy_has_not_been_set_up": "{{strategy}} 尚未设置",
     "ldap_user_not_valid": "Ldap user is no valid",
     "external_account_not_exist": "查找或创建外部账户失败",
 		"maximum_number_of_users": "注册的用户数不能超过最大值。",
@@ -781,7 +774,8 @@
     "Password field is required": "密码字段是必需的",
     "Username or E-mail has invalid characters": "用户名或电子邮件有无效的字符",
     "Password minimum character should be more than 6 characters": "密码最小字符应超过6个字符",
-    "user_not_found": "未找到用户"
+    "user_not_found": "未找到用户",
+    "provider_duplicated_username_exception": "<p><strong><i class='icon-fw icon-ban'></i>发生了重复用户名异常</strong></p><p class='mb-0'> 你的 {{ failedProviderForDuplicatedUsernameException }} 认证成功了,但不能创建新的用户。参见问题<a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
 	},
   "grid_edit":{
     "create_bootstrap_4_grid":"创建Bootstrap 4网格",

+ 76 - 23
packages/app/src/components/LoginForm.tsx

@@ -7,6 +7,8 @@ import { useRouter } from 'next/router';
 import ReactCardFlip from 'react-card-flip';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
+import { LoginErrorCode } from '~/interfaces/errors/login-error';
+import { IErrorV3 } from '~/interfaces/errors/v3-error';
 
 type LoginFormProps = {
   username?: string,
@@ -19,6 +21,7 @@ type LoginFormProps = {
   isPasswordResetEnabled: boolean,
   isLocalStrategySetup: boolean,
   isLdapStrategySetup: boolean,
+  isLdapSetupFailed: boolean,
   objOfIsExternalAuthEnableds?: any,
   isMailerSetup?: boolean
 }
@@ -27,7 +30,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   const router = useRouter();
 
   const {
-    isLocalStrategySetup, isLdapStrategySetup, isPasswordResetEnabled, isRegistrationEnabled,
+    isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled, isRegistrationEnabled,
     isEmailAuthenticationEnabled, registrationMode, registrationWhiteList, isMailerSetup, objOfIsExternalAuthEnableds,
   } = props;
   const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
@@ -38,13 +41,13 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   // For Login
   const [usernameForLogin, setUsernameForLogin] = useState('');
   const [passwordForLogin, setPasswordForLogin] = useState('');
-  const [loginErrors, setLoginErrors] = useState<Error[]>([]);
+  const [loginErrors, setLoginErrors] = useState<IErrorV3[]>([]);
   // 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<IErrorV3[]>([]);
 
   useEffect(() => {
     const { hash } = window.location;
@@ -60,8 +63,14 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     window.location.href = `/passport/${auth}`;
   }, []);
 
+  const resetLoginErrors = useCallback(() => {
+    if (loginErrors.length === 0) return;
+    setLoginErrors([]);
+  }, [loginErrors.length]);
+
   const handleLoginWithLocalSubmit = useCallback(async(e) => {
     e.preventDefault();
+    resetLoginErrors();
 
     const loginForm = {
       username: usernameForLogin,
@@ -78,25 +87,73 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     }
     return;
 
-  }, [passwordForLogin, router, usernameForLogin]);
+  }, [passwordForLogin, resetLoginErrors, router, usernameForLogin]);
+
+  // separate errors based on error code
+  const separateErrorsBasedOnErrorCode = useCallback((errors: IErrorV3[]) => {
+    const loginErrorListForDangerouslySetInnerHTML: IErrorV3[] = [];
+    const loginErrorList: IErrorV3[] = [];
+
+    errors.forEach((err) => {
+      if (err.code === LoginErrorCode.PROVIDER_DUPLICATED_USERNAME_EXCEPTION) {
+        loginErrorListForDangerouslySetInnerHTML.push(err);
+      }
+      else {
+        loginErrorList.push(err);
+      }
+    });
+
+    return [loginErrorListForDangerouslySetInnerHTML, loginErrorList];
+  }, []);
+
+  // wrap error elements which use dangerouslySetInnerHtml
+  const generateDangerouslySetErrors = useCallback((errors: IErrorV3[]): JSX.Element => {
+    if (errors == null || errors.length === 0) return <></>;
+    return (
+      <div className="alert alert-danger">
+        {errors.map((err, index) => {
+          return <small key={index} dangerouslySetInnerHTML={{ __html: t(err.message, err.args) }}></small>;
+        })}
+      </div>
+    );
+  }, [t]);
+
+  // wrap error elements which do not use dangerouslySetInnerHtml
+  const generateSafelySetErrors = useCallback((errors: IErrorV3[]): JSX.Element => {
+    if (errors == null || errors.length === 0) return <></>;
+    return (
+      <ul className="alert alert-danger">
+        {errors.map((err, index) => {
+          return (
+            <li key={index}>
+              {t(err.message, err.args)}<br/>
+            </li>);
+        })}
+      </ul>
+    );
+  }, [t]);
 
   const renderLocalOrLdapLoginForm = useCallback(() => {
     const { isLdapStrategySetup } = props;
+
+    // separate login errors into two arrays based on error code
+    const [loginErrorListForDangerouslySetInnerHTML, loginErrorList] = separateErrorsBasedOnErrorCode(loginErrors);
+    // Generate login error elements using dangerouslySetInnerHTML
+    const loginErrorElementWithDangerouslySetInnerHTML = generateDangerouslySetErrors(loginErrorListForDangerouslySetInnerHTML);
+    // Generate login error elements using <ul>, <li>
+    const loginErrorElement = generateSafelySetErrors(loginErrorList);
+
     return (
       <>
-        {
-          loginErrors != null && loginErrors.length > 0 && (
-            <p className="alert alert-danger">
-              {loginErrors.map((err, index) => {
-                return (
-                  <span key={index}>
-                    {t(err.message)}<br/>
-                  </span>
-                );
-              })}
-            </p>
-          )
-        }
+        {isLdapSetupFailed && (
+          <div className="alert alert-warning small">
+            <strong><i className="icon-fw icon-info"></i>{t('login.enabled_ldap_has_configuration_problem')}</strong><br/>
+            <span dangerouslySetInnerHTML={{ __html: t('login.set_env_var_for_logs') }}></span>
+          </div>
+        )}
+        {loginErrorElementWithDangerouslySetInnerHTML}
+        {loginErrorElement}
+
         <form role="form" onSubmit={handleLoginWithLocalSubmit} id="login-form">
           <div className="input-group">
             <div className="input-group-prepend">
@@ -137,7 +194,8 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
         </form>
       </>
     );
-  }, [handleLoginWithLocalSubmit, loginErrors, props, t]);
+  }, [generateDangerouslySetErrors, generateSafelySetErrors, handleLoginWithLocalSubmit,
+      isLdapSetupFailed, loginErrors, props, separateErrorsBasedOnErrorCode, t]);
 
   const renderExternalAuthInput = useCallback((auth) => {
     const authIconNames = {
@@ -222,11 +280,6 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     return;
   }, [emailForRegister, nameForRegister, passwordForRegister, router, usernameForRegister]);
 
-  const resetLoginErrors = useCallback(() => {
-    if (loginErrors.length === 0) return;
-    setLoginErrors([]);
-  }, [loginErrors.length]);
-
   const resetRegisterErrors = useCallback(() => {
     if (registerErrors.length === 0) return;
     setRegisterErrors([]);

+ 5 - 0
packages/app/src/interfaces/errors/login-error.ts

@@ -0,0 +1,5 @@
+export const LoginErrorCode = {
+  PROVIDER_DUPLICATED_USERNAME_EXCEPTION: 'provider-duplicated-username-exception',
+} as const;
+
+export type LoginErrorCode = typeof LoginErrorCode[keyof typeof LoginErrorCode];

+ 3 - 0
packages/app/src/interfaces/errors/v3-error.ts

@@ -0,0 +1,3 @@
+import { ErrorV3 } from '@growi/core';
+
+export type IErrorV3 = ErrorV3

+ 10 - 2
packages/app/src/pages/login.page.tsx

@@ -26,6 +26,9 @@ type Props = CommonProps & {
   isMailerSetup: boolean,
   enabledStrategies: unknown,
   registrationWhiteList: string[],
+  isLocalStrategySetup: boolean,
+  isLdapStrategySetup: boolean,
+  isLdapSetupFailed: boolean,
 };
 
 const LoginPage: NextPage<Props> = (props: Props) => {
@@ -43,8 +46,9 @@ const LoginPage: NextPage<Props> = (props: Props) => {
       <LoginForm
         // Todo: These props should be set properly. https://redmine.weseek.co.jp/issues/104847
         objOfIsExternalAuthEnableds={props.enabledStrategies}
-        isLocalStrategySetup={true}
-        isLdapStrategySetup={true}
+        isLocalStrategySetup={props.isLocalStrategySetup}
+        isLdapStrategySetup={props.isLdapStrategySetup}
+        isLdapSetupFailed={props.isLdapSetupFailed}
         isEmailAuthenticationEnabled={false}
         isRegistrationEnabled={true}
         registrationWhiteList={props.registrationWhiteList}
@@ -92,9 +96,13 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   const {
     mailService,
     configManager,
+    passportService,
   } = crowi;
 
   props.isMailerSetup = mailService.isMailerSetup;
+  props.isLocalStrategySetup = passportService.isLocalStrategySetup;
+  props.isLdapStrategySetup = passportService.isLdapStrategySetup;
+  props.isLdapSetupFailed = configManager.getConfig('crowi', 'security:passport-ldap:isEnabled') && !props.isLdapStrategySetup;
   props.registrationWhiteList = configManager.getConfig('crowi', 'security:registrationWhiteList');
 }
 

+ 2 - 2
packages/app/src/server/middlewares/apiv3-form-validator.ts

@@ -1,11 +1,11 @@
+import { ErrorV3 } from '@growi/core';
 import { NextFunction, Request, Response } from 'express';
+
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:middlewares:ApiV3FormValidator');
 const { validationResult } = require('express-validator');
 
-const ErrorV3 = require('../models/vo/error-apiv3');
-
 export const apiV3FormValidator = (req: Request, res: Response & { apiv3Err }, next: NextFunction): void => {
   logger.debug('req.query', req.query);
   logger.debug('req.params', req.params);

+ 7 - 12
packages/app/src/server/models/vo/error-apiv3.js → packages/app/src/server/models/vo/error-v3.js

@@ -1,3 +1,6 @@
+// The jsdoc below is left here intentionally to be referenced by swagger's $ref in a file under src/server/routes/apiv3/**/*.js such as healthcheck.js
+// The actual ErrorV3 Class is moved to packages/core/models/vo/error-v3.js
+
 /**
  * @swagger
  *
@@ -15,17 +18,9 @@
  *            example: 'someapi-error-with-something'
  *          stack:
  *            type: object
+ *          args:
+ *            type: object
+ *            example: { name: 'Josh', age: 20 }
  */
 
-class ErrorV3 extends Error {
-
-  constructor(message = '', code = '', stack = undefined) {
-    super(); // do not provide message to the super constructor
-    this.message = message;
-    this.code = code;
-    this.stack = stack;
-  }
-
-}
-
-module.exports = ErrorV3;
+export {};

+ 2 - 3
packages/app/src/server/routes/apiv3/app-settings.js

@@ -1,3 +1,4 @@
+import { ErrorV3 } from '@growi/core';
 import { body } from 'express-validator';
 
 import { i18n } from '^/config/next-i18next.config';
@@ -11,14 +12,12 @@ import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
 const logger = loggerFactory('growi:routes:apiv3:app-settings');
 
+const { pathUtils } = require('@growi/core');
 const debug = require('debug')('growi:routes:admin');
 const express = require('express');
-const { pathUtils } = require('@growi/core');
 
 const router = express.Router();
 
-const ErrorV3 = require('../../models/vo/error-apiv3');
-
 /**
  * @swagger
  *  tags:

+ 2 - 2
packages/app/src/server/routes/apiv3/attachment.js

@@ -1,16 +1,16 @@
+import { ErrorV3 } from '@growi/core';
+
 import loggerFactory from '~/utils/logger';
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
 const logger = loggerFactory('growi:routes:apiv3:attachment'); // eslint-disable-line no-unused-vars
-
 const express = require('express');
 
 const router = express.Router();
 const { query } = require('express-validator');
 
 const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
-const ErrorV3 = require('../../models/vo/error-apiv3');
 
 /**
  * @swagger

+ 2 - 2
packages/app/src/server/routes/apiv3/customize-setting.js

@@ -1,5 +1,7 @@
 /* eslint-disable no-unused-vars */
 
+import { ErrorV3 } from '@growi/core';
+
 import { SupportedAction } from '~/interfaces/activity';
 import { AttachmentType } from '~/server/interfaces/attachment';
 import loggerFactory from '~/utils/logger';
@@ -17,8 +19,6 @@ const router = express.Router();
 const { body, query } = require('express-validator');
 const multer = require('multer');
 
-const ErrorV3 = require('../../models/vo/error-apiv3');
-
 
 /**
  * @swagger

+ 1 - 1
packages/app/src/server/routes/apiv3/forgot-password.js

@@ -1,10 +1,10 @@
+import { ErrorV3 } from '@growi/core';
 import { format, subSeconds } from 'date-fns';
 
 import { SupportedAction } from '~/interfaces/activity';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import injectResetOrderByTokenMiddleware from '~/server/middlewares/inject-reset-order-by-token-middleware';
 import PasswordResetOrder from '~/server/models/password-reset-order';
-import ErrorV3 from '~/server/models/vo/error-apiv3';
 import loggerFactory from '~/utils/logger';
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';

+ 2 - 1
packages/app/src/server/routes/apiv3/healthcheck.js

@@ -1,3 +1,5 @@
+import { ErrorV3 } from '@growi/core';
+
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:routes:apiv3:healthcheck'); // eslint-disable-line no-unused-vars
@@ -7,7 +9,6 @@ const express = require('express');
 const router = express.Router();
 
 const noCache = require('nocache');
-const ErrorV3 = require('../../models/vo/error-apiv3');
 
 /**
  * @swagger

+ 1 - 2
packages/app/src/server/routes/apiv3/import.js

@@ -1,3 +1,4 @@
+import { ErrorV3 } from '@growi/core';
 import mongoose from 'mongoose';
 
 import { SupportedAction } from '~/interfaces/activity';
@@ -16,8 +17,6 @@ const multer = require('multer');
 
 const GrowiArchiveImportOption = require('~/models/admin/growi-archive-import-option');
 
-const ErrorV3 = require('../../models/vo/error-apiv3');
-
 
 const router = express.Router();
 

+ 1 - 1
packages/app/src/server/routes/apiv3/installer.ts

@@ -1,7 +1,7 @@
+import { ErrorV3 } from '@growi/core';
 import express, { Request, Router } from 'express';
 
 import { SupportedAction } from '~/interfaces/activity';
-import ErrorV3 from '~/server/models/vo/error-apiv3';
 import loggerFactory from '~/utils/logger';
 
 import Crowi from '../../crowi';

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

@@ -1,3 +1,5 @@
+import { ErrorV3 } from '@growi/core';
+
 import { SupportedAction } from '~/interfaces/activity';
 import loggerFactory from '~/utils/logger';
 
@@ -13,8 +15,6 @@ const router = express.Router();
 
 const { body } = require('express-validator');
 
-const ErrorV3 = require('../../models/vo/error-apiv3');
-
 const validator = {
   lineBreak: [
     body('isEnabledLinebreaks').isBoolean(),

+ 2 - 2
packages/app/src/server/routes/apiv3/notification-setting.js

@@ -1,3 +1,5 @@
+import { ErrorV3 } from '@growi/core';
+
 import { SupportedAction } from '~/interfaces/activity';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
@@ -16,8 +18,6 @@ const router = express.Router();
 
 const { body } = require('express-validator');
 
-const ErrorV3 = require('../../models/vo/error-apiv3');
-
 const validator = {
   userNotification: [
     body('pathPattern').isString().trim(),

+ 1 - 1
packages/app/src/server/routes/apiv3/page-listing.ts

@@ -1,3 +1,4 @@
+import { ErrorV3 } from '@growi/core';
 import express, { Request, Router } from 'express';
 import { query, oneOf } from 'express-validator';
 import mongoose from 'mongoose';
@@ -9,7 +10,6 @@ import loggerFactory from '~/utils/logger';
 import Crowi from '../../crowi';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import { PageModel } from '../../models/page';
-import ErrorV3 from '../../models/vo/error-apiv3';
 import PageService from '../../service/page';
 
 import { ApiV3Response } from './interfaces/apiv3-response';

+ 3 - 2
packages/app/src/server/routes/apiv3/page.js

@@ -1,4 +1,6 @@
-import { pagePathUtils, AllSubscriptionStatusType, SubscriptionStatusType } from '@growi/core';
+import {
+  pagePathUtils, AllSubscriptionStatusType, SubscriptionStatusType, ErrorV3,
+} from '@growi/core';
 
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
@@ -15,7 +17,6 @@ const { body, query, param } = require('express-validator');
 
 const router = express.Router();
 const { convertToNewAffiliationPath, isTopPage } = pagePathUtils;
-const ErrorV3 = require('../../models/vo/error-apiv3');
 
 /**
  * @swagger

+ 2 - 2
packages/app/src/server/routes/apiv3/pages.js

@@ -1,3 +1,5 @@
+import { ErrorV3 } from '@growi/core';
+
 import { SupportedTargetModel, SupportedAction } from '~/interfaces/activity';
 import { subscribeRuleNames } from '~/interfaces/in-app-notification';
 import loggerFactory from '~/utils/logger';
@@ -13,8 +15,6 @@ const { body } = require('express-validator');
 const { query } = require('express-validator');
 const mongoose = require('mongoose');
 
-const ErrorV3 = require('../../models/vo/error-apiv3');
-
 const { isCreatablePage } = pagePathUtils;
 
 const router = express.Router();

+ 2 - 1
packages/app/src/server/routes/apiv3/response.js

@@ -1,6 +1,7 @@
+import { ErrorV3 } from '@growi/core';
+
 import { toArrayIfNot } from '~/utils/array-utils';
 
-const ErrorV3 = require('../../models/vo/error-apiv3');
 
 const addCustomFunctionToResponse = (express, crowi) => {
 

+ 3 - 2
packages/app/src/server/routes/apiv3/revisions.js

@@ -1,3 +1,5 @@
+import { ErrorV3 } from '@growi/core';
+
 import loggerFactory from '~/utils/logger';
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
@@ -5,10 +7,9 @@ import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 const logger = loggerFactory('growi:routes:apiv3:pages');
 
 const express = require('express');
-
 const { query, param } = require('express-validator');
+
 const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
-const ErrorV3 = require('../../models/vo/error-apiv3');
 
 const router = express.Router();
 

+ 2 - 2
packages/app/src/server/routes/apiv3/search.js

@@ -1,3 +1,5 @@
+import { ErrorV3 } from '@growi/core';
+
 import { SupportedAction } from '~/interfaces/activity';
 import loggerFactory from '~/utils/logger';
 
@@ -14,8 +16,6 @@ const router = express.Router();
 
 const noCache = require('nocache');
 
-const ErrorV3 = require('../../models/vo/error-apiv3');
-
 /**
  * @swagger
  *  tags:

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

@@ -1,3 +1,5 @@
+import { ErrorV3 } from '@growi/core';
+
 import { SupportedAction } from '~/interfaces/activity';
 import { PageDeleteConfigValue } from '~/interfaces/page-delete-config';
 import loggerFactory from '~/utils/logger';
@@ -16,8 +18,6 @@ const router = express.Router();
 
 const { body } = require('express-validator');
 
-const ErrorV3 = require('../../models/vo/error-apiv3');
-
 const validator = {
   generalSetting: [
     body('sessionMaxAge').optional({ checkFalsy: true }).trim().isInt(),

+ 2 - 2
packages/app/src/server/routes/apiv3/share-links.js

@@ -1,5 +1,7 @@
 // TODO remove this setting after implemented all
 /* eslint-disable no-unused-vars */
+import { ErrorV3 } from '@growi/core';
+
 import { SupportedAction } from '~/interfaces/activity';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import loggerFactory from '~/utils/logger';
@@ -15,8 +17,6 @@ const router = express.Router();
 
 const { body, query, param } = require('express-validator');
 
-const ErrorV3 = require('../../models/vo/error-apiv3');
-
 const validator = {};
 
 const today = new Date();

+ 2 - 2
packages/app/src/server/routes/apiv3/slack-integration-legacy-settings.js

@@ -1,3 +1,5 @@
+import { ErrorV3 } from '@growi/core';
+
 import { SupportedAction } from '~/interfaces/activity';
 import loggerFactory from '~/utils/logger';
 
@@ -14,8 +16,6 @@ const router = express.Router();
 
 const { body } = require('express-validator');
 
-const ErrorV3 = require('../../models/vo/error-apiv3');
-
 const validator = {
   slackConfiguration: [
     body('webhookUrl').if(value => value != null).isString().trim(),

+ 1 - 2
packages/app/src/server/routes/apiv3/slack-integration-settings.js

@@ -1,3 +1,4 @@
+import { ErrorV3 } from '@growi/core';
 
 import { SupportedAction } from '~/interfaces/activity';
 import loggerFactory from '~/utils/logger';
@@ -20,8 +21,6 @@ const { body, query, param } = require('express-validator');
 const mongoose = require('mongoose');
 const urljoin = require('url-join');
 
-const ErrorV3 = require('../../models/vo/error-apiv3');
-
 const logger = loggerFactory('growi:routes:apiv3:slack-integration-settings');
 
 const router = express.Router();

+ 1 - 2
packages/app/src/server/routes/apiv3/slack-integration.js

@@ -1,10 +1,9 @@
+import { ErrorV3 } from '@growi/core';
 import createError from 'http-errors';
 
 import { SlackCommandHandlerError } from '~/server/models/vo/slack-command-handler-error';
 import loggerFactory from '~/utils/logger';
 
-import ErrorV3 from '../../models/vo/error-apiv3';
-
 import {
   markdownSectionBlock, InvalidGrowiCommandError, generateRespondUtil, supportedGrowiCommands,
 } from '@growi/slack';

+ 2 - 1
packages/app/src/server/routes/apiv3/user-activation.ts

@@ -1,7 +1,8 @@
 import path from 'path';
+
+import { ErrorV3 } from '@growi/core';
 import * as express from 'express';
 import { body, validationResult } from 'express-validator';
-import ErrorV3 from '../../models/vo/error-apiv3';
 
 const PASSOWRD_MINIMUM_NUMBER = 8;
 // validation rules for complete registration form

+ 2 - 1
packages/app/src/server/routes/apiv3/user-group-relation.js

@@ -1,3 +1,5 @@
+import { ErrorV3 } from '@growi/core';
+
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:routes:apiv3:user-group-relation'); // eslint-disable-line no-unused-vars
@@ -5,7 +7,6 @@ const logger = loggerFactory('growi:routes:apiv3:user-group-relation'); // eslin
 const express = require('express');
 const { query } = require('express-validator');
 
-const ErrorV3 = require('../../models/vo/error-apiv3');
 const { serializeUserGroupRelationSecurely } = require('../../models/serializers/user-group-relation-serializer');
 
 const router = express.Router();

+ 2 - 1
packages/app/src/server/routes/apiv3/user-group.js

@@ -1,3 +1,5 @@
+import { ErrorV3 } from '@growi/core';
+
 import { SupportedAction } from '~/interfaces/activity';
 import UserGroup from '~/server/models/user-group';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
@@ -18,7 +20,6 @@ const { sanitizeQuery } = require('express-validator');
 const mongoose = require('mongoose');
 
 const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
-const ErrorV3 = require('../../models/vo/error-apiv3');
 const { toPagingLimit, toPagingOffset } = require('../../util/express-validator/sanitizer');
 
 const { ObjectId } = mongoose.Types;

+ 2 - 3
packages/app/src/server/routes/apiv3/user-ui-settings.ts

@@ -1,13 +1,12 @@
+import { ErrorV3 } from '@growi/core';
 import express from 'express';
 import { body } from 'express-validator';
-import { AllSidebarContentsType } from '~/interfaces/ui';
 
+import { AllSidebarContentsType } from '~/interfaces/ui';
 import loggerFactory from '~/utils/logger';
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
-
 import UserUISettings from '../../models/user-ui-settings';
-import ErrorV3 from '../../models/vo/error-apiv3';
 
 const logger = loggerFactory('growi:routes:apiv3:user-ui-settings');
 

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

@@ -1,3 +1,5 @@
+import { ErrorV3 } from '@growi/core';
+
 import { SupportedAction } from '~/interfaces/activity';
 import Activity from '~/server/models/activity';
 import loggerFactory from '~/utils/logger';
@@ -19,7 +21,6 @@ const { isEmail } = require('validator');
 
 const { serializePageSecurely } = require('../../models/serializers/page-serializer');
 const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
-const ErrorV3 = require('../../models/vo/error-apiv3');
 
 const PAGE_ITEMS = 50;
 

+ 17 - 10
packages/app/src/server/routes/login-passport.js

@@ -1,8 +1,11 @@
+
+import { ErrorV3 } from '@growi/core';
+
 import { SupportedAction } from '~/interfaces/activity';
+import { LoginErrorCode } from '~/interfaces/errors/login-error';
 import { NullUsernameToBeRegisteredError } from '~/server/models/errors';
 import loggerFactory from '~/utils/logger';
 
-import ErrorV3 from '../models/vo/error-apiv3';
 
 /* eslint-disable no-use-before-define */
 
@@ -67,7 +70,7 @@ module.exports = function(crowi, app) {
       /* eslint-disable no-else-return */
       if (err instanceof NullUsernameToBeRegisteredError) {
         logger.error(err.message);
-        throw Error(err.message);
+        throw new ErrorV3(err.message);
       }
       else if (err.name === 'DuplicatedUsernameException') {
         if (isSameEmailTreatedAsIdenticalUser || isSameUsernameTreatedAsIdenticalUser) {
@@ -76,11 +79,13 @@ module.exports = function(crowi, app) {
           return ExternalAccount.associate(providerId, userInfo.id, err.user);
         }
         logger.error('provider-DuplicatedUsernameException', providerId);
-        throw Error(`provider-DuplicatedUsernameException: ${providerId}`);
+
+        throw new ErrorV3('message.provider_duplicated_username_exception', LoginErrorCode.PROVIDER_DUPLICATED_USERNAME_EXCEPTION,
+          undefined, { failedProviderForDuplicatedUsernameException: providerId });
       }
       else if (err.name === 'UserUpperLimitException') {
         logger.error(err.message);
-        throw Error(err.message);
+        throw new ErrorV3(err.message);
       }
       /* eslint-enable no-else-return */
     }
@@ -138,21 +143,23 @@ module.exports = function(crowi, app) {
 
   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');
+    const err = new ErrorV3('message.sign_in_failure');
     return next(err);
   };
 
   /**
    * middleware for login failure
+   * @param {*} error
    * @param {*} req
    * @param {*} res
+   * @param {*} next
    */
   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);
+    return res.apiv3Err(error);
   };
 
   /**
@@ -180,7 +187,7 @@ module.exports = function(crowi, app) {
   const loginWithLdap = async(req, res, next) => {
     if (!passportService.isLdapStrategySetup) {
       debug('LdapStrategy has not been set up');
-      return res.apiv3Err('message.strategy_has_not_been_set_up.LdapStrategy', 405);
+      return next();
     }
 
     if (!req.form.isValid) {
@@ -201,7 +208,7 @@ module.exports = function(crowi, app) {
 
     // check groups for LDAP
     if (!isValidLdapUserByGroupFilter(ldapAccountInfo)) {
-      return next(ErrorV3('message.ldap_user_not_valid', 400));
+      return next(new ErrorV3('message.ldap_user_not_valid'));
     }
 
     /*
@@ -234,7 +241,7 @@ module.exports = function(crowi, app) {
 
     // just in case the returned value is null or undefined
     if (externalAccount == null) {
-      return next(Error('message.external_account_not_exist'));
+      return next(new ErrorV3('message.external_account_not_exist'));
     }
 
     const user = await externalAccount.getPopulatedUser();
@@ -315,7 +322,7 @@ module.exports = function(crowi, app) {
   const loginWithLocal = (req, res, next) => {
     if (!passportService.isLocalStrategySetup) {
       debug('LocalStrategy has not been set up');
-      return res.apiv3Err('message.strategy_has_not_been_set_up.LocalStrategy', 405);
+      return res.apiv3Err(new ErrorV3('message.strategy_has_not_been_set_up', '', undefined, { strategy: 'LocalStrategy' }), 405);
     }
 
     if (!req.form.isValid) {

+ 1 - 0
packages/core/src/index.ts

@@ -24,6 +24,7 @@ export * from './interfaces/tag';
 export * from './interfaces/user';
 export * from './plugin/service/tag-cache-manager';
 export * from './models/devided-page-path';
+export * from './models/vo/error-apiv3';
 export * from './service/localstorage-manager';
 export * from './utils/basic-interceptor';
 export * from './utils/browser-utils';

+ 15 - 0
packages/core/src/models/vo/error-apiv3.ts

@@ -0,0 +1,15 @@
+export class ErrorV3 extends Error {
+
+  code: string;
+
+  args?: any;
+
+  constructor(message = '', code = '', stack = undefined, args = undefined) {
+    super(); // do not provide message to the super constructor
+    this.message = message;
+    this.code = code;
+    this.stack = stack;
+    this.args = args;
+  }
+
+}