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

Merge branch 'support/apply-nextjs-2' into fix/UserPage-delete-deleted-user-info

jam411 3 лет назад
Родитель
Сommit
6aeb11711b

+ 1 - 1
packages/app/package.json

@@ -130,7 +130,7 @@
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "next": "^12.2.5",
-    "next-i18next": "^11.0.0",
+    "next-i18next": "^11.3.0",
     "next-superjson": "^0.0.4",
     "next-themes": "^0.2.0",
     "nocache": "^3.0.1",

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

@@ -709,7 +709,17 @@
     "complete_to_install2":"Complete to Install GROWI ! Please check each settings on this page first.",
     "failed_to_create_admin_user":"Failed to create admin user. {{errMessage}}",
     "successfully_send_email_auth":"We sent an email to {{email}}. Please click the URL in the email and complete the registration.",
-    "incorrect_token_or_expired_url": "The token is incorrect or the URL has expired."
+    "incorrect_token_or_expired_url": "The token is incorrect or the URL has expired.",
+    "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.",
+    "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"
   },
   "grid_edit":{
     "create_bootstrap_4_grid":"Create Bootstrap 4 Grid",

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

@@ -700,7 +700,17 @@
     "complete_to_install2":"GROWI のインストールが完了しました!はじめに、このページで各種設定を確認してください。",
     "failed_to_create_admin_user":"管理ユーザーの作成に失敗しました。{{errMessage}}",
     "successfully_send_email_auth":"{{email}} にメールを送信しました。添付されたURLをクリックし、本登録を完了させてください",
-    "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。"
+    "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。",
+    "user_already_loggedin": "ログイン中のため、新規アカウントを作成できませんでした。",
+    "registration_closed": "新しいアカウントを作成する権限がありません。",
+    "Username has invalid characters": "ユーザー名に不正な文字が含まれています.",
+    "Username field is required": "User ID は必須項目です",
+    "Name field is required": "ユーザーID は必須項目です",
+    "Email format is invalid": "メールアドレスのフォーマットが無効です",
+    "Email field is required": "メールアドレスは必須項目です",
+    "Password has invalid character": "パスワードに無効な文字があります",
+    "Password minimum character should be more than 8 characters": "パスワードの最小文字数は8文字以上です",
+    "Password field is required": "パスワードの欄は必ず入力してください"
   },
   "grid_edit":{
     "create_bootstrap_4_grid":"Bootstrap 4 グリッドを作成",

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

@@ -756,7 +756,17 @@
 		"complete_to_install2": "完成安装GROWI!请先检查此页上的每个设置。",
 		"failed_to_create_admin_user": "无法创建管理用户。{{errMessage}",
     "successfully_send_email_auth":"我们向 {{email}} 发送了一封电子邮件。 请点击邮件中的网址并完成注册。",
-    "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。"
+    "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。",
+    "user_already_loggedin": "当你登录的时候,你不能创建一个新的账户。",
+    "registration_closed": "你无权创建一个新的账户。",
+    "Username has invalid characters": "用户名有无效字符",
+    "Username field is required": "用户ID字段是必需的",
+    "Name field is required": "姓名字段为必填项",
+    "Email format is invalid": "电子邮件的格式是无效的",
+    "Email field is required": "电子邮件字段是必需的",
+    "Password has invalid character": "密码有无效字符",
+    "Password minimum character should be more than 8 characters": "密码最小字符应超过8个字符",
+    "Password field is required": "密码字段是必需的"
 	},
   "grid_edit":{
     "create_bootstrap_4_grid":"创建Bootstrap 4网格",

+ 3 - 4
packages/app/src/components/BookmarkButtons.module.scss

@@ -1,12 +1,11 @@
 @use '~/styles/bootstrap/init' as bs;
 
-.btn-bookmark {
-  :global {
+.btn-group-bookmark :global {
+  .btn-bookmark {
     box-shadow: none !important;
-  }
 
-  &:global {
     @include bs.button-outline-variant(rgba(bs.$secondary, 50%), bs.$orange, rgba(lighten(bs.$orange, 20%), 0.5), rgba(lighten(bs.$orange, 20%), 0.5));
+
     &:not(:disabled):not(.disabled):active,
     &:not(:disabled):not(.disabled).active {
       color: bs.$orange;

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

@@ -52,12 +52,12 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
   }, [isGuestUser, isBookmarked]);
 
   return (
-    <div className="btn-group" role="group" aria-label="Bookmark buttons">
+    <div className={`btn-group btn-group-bookmark ${styles['btn-group-bookmark']}`} role="group" aria-label="Bookmark buttons">
       <button
         type="button"
         id="bookmark-button"
         onClick={handleClick}
-        className={`shadow-none btn btn-bookmark ${styles['btn-bookmark']} border-0
+        className={`shadow-none btn btn-bookmark border-0
           ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
         <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>

+ 4 - 4
packages/app/src/components/LikeButtons.module.scss

@@ -1,11 +1,11 @@
 @use '~/styles/bootstrap/init' as bs;
 
-.btn-like {
-  :global {
+.btn-group-like :global {
+  .btn-like {
     box-shadow: none !important;
-  }
-  &:global {
+
     @include bs.button-outline-variant(rgba(bs.$secondary, 50%), lighten(bs.$red, 15%), rgba(lighten(bs.$red, 10%), 0.15), rgba(lighten(bs.$red, 10%), 0.5));
+
     &:not(:disabled):not(.disabled):active,
     &:not(:disabled):not(.disabled).active {
       color: lighten(bs.$red, 15%);

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

@@ -45,12 +45,12 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
   }, [isGuestUser, isLiked]);
 
   return (
-    <div className="btn-group" role="group" aria-label="Like buttons">
+    <div className={`btn-group btn-group-like ${styles['btn-group-like']}`} role="group" aria-label="Like buttons">
       <button
         type="button"
         id="like-button"
         onClick={onLikeClicked}
-        className={`shadow-none btn btn-like ${styles['btn-like']} border-0
+        className={`shadow-none btn btn-like border-0
             ${isLiked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
         <i className={`fa ${isLiked ? 'fa-heart' : 'fa-heart-o'}`}></i>

+ 174 - 144
packages/app/src/components/LoginForm.jsx → packages/app/src/components/LoginForm.tsx

@@ -1,44 +1,64 @@
-import React from 'react';
+import React, {
+  useState, useEffect, useCallback,
+} from 'react';
 
 import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
+import { useRouter } from 'next/router';
 import ReactCardFlip from 'react-card-flip';
 
+import { apiv3Post } from '~/client/util/apiv3-client';
 import { useCsrfToken } from '~/stores/context';
 
-class LoginForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      isRegistering: false,
-    };
-
-    this.switchForm = this.switchForm.bind(this);
-    this.handleLoginWithExternalAuth = this.handleLoginWithExternalAuth.bind(this);
-    this.renderLocalOrLdapLoginForm = this.renderLocalOrLdapLoginForm.bind(this);
-    this.renderExternalAuthLoginForm = this.renderExternalAuthLoginForm.bind(this);
-    this.renderExternalAuthInput = this.renderExternalAuthInput.bind(this);
-    this.renderRegisterForm = this.renderRegisterForm.bind(this);
+type LoginFormProps = {
+  username?: string,
+  name?: string,
+  email?: string,
+  isRegistrationEnabled: boolean,
+  isEmailAuthenticationEnabled: boolean,
+  registrationMode?: string,
+  registrationWhiteList: string[],
+  isPasswordResetEnabled: boolean,
+  isLocalStrategySetup: boolean,
+  isLdapStrategySetup: boolean,
+  objOfIsExternalAuthEnableds?: any,
+  isMailerSetup?: boolean
+}
+export const LoginForm = (props: LoginFormProps): JSX.Element => {
+  const { t } = useTranslation();
+  const router = useRouter();
+  const { data: csrfToken } = useCsrfToken();
 
+  const {
+    isLocalStrategySetup, isLdapStrategySetup, isPasswordResetEnabled, isRegistrationEnabled,
+    isEmailAuthenticationEnabled, registrationMode, registrationWhiteList, isMailerSetup,
+  } = props;
+  const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
+  // const isSomeExternalAuthEnabled = Object.values(objOfIsExternalAuthEnableds).some(elem => elem);
+  const isSomeExternalAuthEnabled = true;
+
+  // states
+  const [isRegistering, setIsRegistering] = useState(false);
+  const [username, setUsername] = useState('');
+  const [name, setName] = useState('');
+  const [email, setEmail] = useState('');
+  const [password, setPassword] = useState('');
+  const [registerErrors, setRegisterErrors] = useState<Error[]>([]);
+
+  useEffect(() => {
     const { hash } = window.location;
     if (hash === '#register') {
-      this.state.isRegistering = true;
+      setIsRegistering(true);
     }
-  }
-
-  switchForm() {
-    this.setState({ isRegistering: !this.state.isRegistering });
-  }
+  }, []);
 
-  handleLoginWithExternalAuth(e) {
+  // functions
+  const handleLoginWithExternalAuth = useCallback((e) => {
     const auth = e.currentTarget.id;
-    window.location.href = `/passport/${auth}`;
-  }
 
-  renderLocalOrLdapLoginForm() {
-    const { t, csrfToken, isLdapStrategySetup } = this.props;
+    window.location.href = `/passport/${auth}`;
+  }, []);
+  const renderLocalOrLdapLoginForm = useCallback(() => {
+    const { isLdapStrategySetup } = props;
 
     return (
       <form role="form" action="/login" method="post">
@@ -79,10 +99,8 @@ class LoginForm extends React.Component {
         </div>
       </form>
     );
-  }
-
-  renderExternalAuthInput(auth) {
-    const { t } = this.props;
+  }, [csrfToken, props, t]);
+  const renderExternalAuthInput = useCallback((auth) => {
     const authIconNames = {
       google: 'google',
       github: 'github',
@@ -95,7 +113,7 @@ class LoginForm extends React.Component {
 
     return (
       <div key={auth} className="col-6 my-2">
-        <button type="button" className="btn btn-fill rounded-0" id={auth} onClick={this.handleLoginWithExternalAuth}>
+        <button type="button" className="btn btn-fill rounded-0" id={auth} onClick={handleLoginWithExternalAuth}>
           <div className="eff"></div>
           <span className="btn-label">
             <i className={`fa fa-${authIconNames[auth]}`}></i>
@@ -105,10 +123,9 @@ class LoginForm extends React.Component {
         <div className="small text-right">by {auth} Account</div>
       </div>
     );
-  }
-
-  renderExternalAuthLoginForm() {
-    const { isLocalStrategySetup, isLdapStrategySetup, objOfIsExternalAuthEnableds } = this.props;
+  }, [handleLoginWithExternalAuth, t]);
+  const renderExternalAuthLoginForm = useCallback(() => {
+    const { isLocalStrategySetup, isLdapStrategySetup, objOfIsExternalAuthEnableds } = props;
     const isExternalAuthCollapsible = isLocalStrategySetup || isLdapStrategySetup;
     const collapsibleClass = isExternalAuthCollapsible ? 'collapse collapse-external-auth' : '';
 
@@ -121,7 +138,7 @@ class LoginForm extends React.Component {
                 if (!objOfIsExternalAuthEnableds[auth]) {
                   return;
                 }
-                return this.renderExternalAuthInput(auth);
+                return renderExternalAuthInput(auth);
               })}
             </div>
           </div>
@@ -140,22 +157,42 @@ class LoginForm extends React.Component {
         </div>
       </>
     );
-  }
+  }, [props, renderExternalAuthInput]);
 
-  renderRegisterForm() {
-    const {
-      t,
-      // appContainer,
-      csrfToken,
-      isEmailAuthenticationEnabled,
+  const handleRegisterFormSubmit = useCallback(async(e, requestPath) => {
+    e.preventDefault();
+
+    const registerForm = {
       username,
       name,
       email,
-      registrationMode,
-      registrationWhiteList,
-      isMailerSetup,
-    } = this.props;
+      password,
+    };
+    try {
+      const res = await apiv3Post(requestPath, { registerForm });
+      const { redirectTo } = res.data;
+      router.push(redirectTo);
+    }
+    catch (err) {
+      // Execute if error exists
+      if (err != null || err.length > 0) {
+        setRegisterErrors(err);
+      }
+    }
+    return;
+  }, [email, name, password, router, username]);
+
+  const resetRegisterErrors = useCallback(() => {
+    if (registerErrors.length === 0) return;
+    setRegisterErrors([]);
+  }, [registerErrors.length]);
+
+  const switchForm = useCallback(() => {
+    setIsRegistering(!isRegistering);
+    resetRegisterErrors();
+  }, [isRegistering, resetRegisterErrors]);
 
+  const renderRegisterForm = useCallback(() => {
     let registerAction = '/register';
 
     let submitText = t('Sign up');
@@ -179,7 +216,21 @@ class LoginForm extends React.Component {
           </p>
         )}
 
-        <form role="form" action={registerAction} method="post" id="register-form">
+        {
+          registerErrors != null && registerErrors.length > 0 && (
+            <p className="alert alert-danger">
+              {registerErrors.map((err, index) => {
+                return (
+                  <span key={index}>
+                    {t(`message.${err.message}`)}<br/>
+                  </span>
+                );
+              })}
+            </p>
+          )
+        }
+
+        <form role="form" onSubmit={e => handleRegisterFormSubmit(e, registerAction) } id="register-form">
 
           {!isEmailAuthenticationEnabled && (
             <div>
@@ -189,12 +240,14 @@ class LoginForm extends React.Component {
                     <i className="icon-user"></i>
                   </span>
                 </div>
+                {/* username */}
                 <input
                   type="text"
                   className="form-control rounded-0"
+                  onChange={(e) => { setUsername(e.target.value) }}
                   placeholder={t('User ID')}
-                  name="registerForm[username]"
-                  defaultValue={username}
+                  name="username"
+                  defaultValue={props.username}
                   required
                 />
               </div>
@@ -207,7 +260,14 @@ class LoginForm extends React.Component {
                     <i className="icon-tag"></i>
                   </span>
                 </div>
-                <input type="text" className="form-control rounded-0" placeholder={t('Name')} name="registerForm[name]" defaultValue={name} required />
+                {/* name */}
+                <input type="text"
+                  className="form-control rounded-0"
+                  onChange={(e) => { setName(e.target.value) }}
+                  placeholder={t('Name')}
+                  name="name"
+                  defaultValue={props.name}
+                  required />
               </div>
             </div>
           )}
@@ -218,7 +278,15 @@ class LoginForm extends React.Component {
                 <i className="icon-envelope"></i>
               </span>
             </div>
-            <input type="email" className="form-control rounded-0" placeholder={t('Email')} name="registerForm[email]" defaultValue={email} required />
+            {/* email */}
+            <input type="email"
+              className="form-control rounded-0"
+              onChange={(e) => { setEmail(e.target.value) }}
+              placeholder={t('Email')}
+              name="email"
+              defaultValue={props.email}
+              required
+            />
           </div>
 
           {registrationWhiteList.length > 0 && (
@@ -244,14 +312,24 @@ class LoginForm extends React.Component {
                     <i className="icon-lock"></i>
                   </span>
                 </div>
-                <input type="password" className="form-control rounded-0" placeholder={t('Password')} name="registerForm[password]" required />
+                {/* Password */}
+                <input type="password"
+                  className="form-control rounded-0"
+                  onChange={(e) => { setPassword(e.target.value) }}
+                  placeholder={t('Password')}
+                  name="password"
+                  required />
               </div>
             </div>
           )}
 
+          {/* Sign up button (submit) */}
           <div className="input-group justify-content-center my-4">
-            <input type="hidden" name="_csrf" value={csrfToken} />
-            <button type="submit" className="btn btn-fill rounded-0" id="register" disabled={(!isMailerSetup && isEmailAuthenticationEnabled)}>
+            <button
+              className="btn btn-fill rounded-0"
+              id="register"
+              disabled={(!isMailerSetup && isEmailAuthenticationEnabled)}
+            >
               <div className="eff"></div>
               <span className="btn-label">
                 <i className="icon-user-follow"></i>
@@ -265,7 +343,7 @@ class LoginForm extends React.Component {
 
         <div className="row">
           <div className="text-right col-12 mt-2 py-2">
-            <a href="#login" id="login" className="link-switch" onClick={this.switchForm}>
+            <a href="#login" id="login" className="link-switch" onClick={switchForm}>
               <i className="icon-fw icon-login"></i>
               {t('Sign in is here')}
             </a>
@@ -273,93 +351,45 @@ class LoginForm extends React.Component {
         </div>
       </React.Fragment>
     );
-  }
-
-  render() {
-    const {
-      t,
-      isLocalStrategySetup,
-      isLdapStrategySetup,
-      isRegistrationEnabled,
-      isPasswordResetEnabled,
-      objOfIsExternalAuthEnableds,
-    } = this.props;
-
-
-    const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
-    // const isSomeExternalAuthEnabled = Object.values(objOfIsExternalAuthEnableds).some(elem => elem);
-    const isSomeExternalAuthEnabled = true;
-
-    return (
-      <div className="noLogin-dialog mx-auto" id="noLogin-dialog">
-        <div className="row mx-0">
-          <div className="col-12">
-            <ReactCardFlip isFlipped={this.state.isRegistering} flipDirection="horizontal" cardZIndex="3">
-              <div className="front">
-                {isLocalOrLdapStrategiesEnabled && this.renderLocalOrLdapLoginForm()}
-                {isSomeExternalAuthEnabled && this.renderExternalAuthLoginForm()}
-                {isLocalOrLdapStrategiesEnabled && isPasswordResetEnabled && (
-                  <div className="text-right mb-2">
-                    <a href="/forgot-password" className="d-block link-switch">
-                      <i className="icon-key"></i> {t('forgot_password.forgot_password')}
-                    </a>
-                  </div>
-                )}
-                {isRegistrationEnabled && (
-                  <div className="text-right mb-2">
-                    <a href="#register" id="register" className="link-switch" onClick={this.switchForm}>
-                      <i className="ti ti-check-box"></i> {t('Sign up is here')}
-                    </a>
-                  </div>
-                )}
-              </div>
-              <div className="back">
-                {isRegistrationEnabled && this.renderRegisterForm()}
-              </div>
-            </ReactCardFlip>
-          </div>
+  }, [handleRegisterFormSubmit, isEmailAuthenticationEnabled, isMailerSetup,
+      props.email, props.name, props.username,
+      registerErrors, registrationMode, registrationWhiteList, switchForm, t]);
+
+  return (
+    <div className="noLogin-dialog mx-auto" id="noLogin-dialog">
+      <div className="row mx-0">
+        <div className="col-12">
+          <ReactCardFlip isFlipped={isRegistering} flipDirection="horizontal" cardZIndex="3">
+            <div className="front">
+              {isLocalOrLdapStrategiesEnabled && renderLocalOrLdapLoginForm()}
+              {isSomeExternalAuthEnabled && renderExternalAuthLoginForm()}
+              {isLocalOrLdapStrategiesEnabled && isPasswordResetEnabled && (
+                <div className="text-right mb-2">
+                  <a href="/forgot-password" className="d-block link-switch">
+                    <i className="icon-key"></i> {t('forgot_password.forgot_password')}
+                  </a>
+                </div>
+              )}
+              {/* Sign up link */}
+              {isRegistrationEnabled && (
+                <div className="text-right mb-2">
+                  <a href="#register" id="register" className="link-switch" onClick={switchForm}>
+                    <i className="ti ti-check-box"></i> {t('Sign up is here')}
+                  </a>
+                </div>
+              )}
+            </div>
+            <div className="back">
+              {/* Register form for /login#register */}
+              {isRegistrationEnabled && renderRegisterForm()}
+            </div>
+          </ReactCardFlip>
         </div>
-        <a href="https://growi.org" className="link-growi-org pl-3">
-          <span className="growi">GROWI</span>.<span className="org">ORG</span>
-        </a>
       </div>
-    );
-  }
-
-}
+      <a href="https://growi.org" className="link-growi-org pl-3">
+        <span className="growi">GROWI</span>.<span className="org">ORG</span>
+      </a>
+    </div>
+  );
 
-LoginForm.propTypes = {
-  // i18next
-  t: PropTypes.func.isRequired,
-  // appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  csrfToken: PropTypes.string,
-  isRegistering: PropTypes.bool,
-  username: PropTypes.string,
-  name: PropTypes.string,
-  email: PropTypes.string,
-  isRegistrationEnabled: PropTypes.bool,
-  registrationMode: PropTypes.string,
-  registrationWhiteList: PropTypes.array,
-  isPasswordResetEnabled: PropTypes.bool,
-  isEmailAuthenticationEnabled: PropTypes.bool,
-  isLocalStrategySetup: PropTypes.bool,
-  isLdapStrategySetup: PropTypes.bool,
-  objOfIsExternalAuthEnableds: PropTypes.object,
-  isMailerSetup: PropTypes.bool,
 };
-
-const LoginFormWrapperFC = (props) => {
-  const { t } = useTranslation();
-  const { data: csrfToken } = useCsrfToken();
-
-  return <LoginForm t={t} csrfToken={csrfToken} {...props} />;
-};
-
-/**
- * Wrapper component for using unstated
- */
-// const LoginFormWrapper = withUnstatedContainers(LoginFormWrapperFC, [AppContainer]);
-
-// export default LoginForm;
-export default LoginFormWrapperFC;

+ 0 - 1
packages/app/src/components/Me/BasicInfoSettings.tsx

@@ -109,7 +109,6 @@ export const BasicInfoSettings = (): JSX.Element => {
             i18nConfig.locales.map((locale) => {
               if (i18n == null) { return }
               const fixedT = i18n.getFixedT(locale);
-              i18n.loadLanguages(i18nConfig.locales);
 
               return (
                 <div key={locale} className="custom-control custom-radio custom-control-inline">

+ 22 - 0
packages/app/src/components/User/SeenUserInfo.module.scss

@@ -0,0 +1,22 @@
+@use '~/styles/bootstrap/init' as bs;
+@use '~/styles/mixins';
+
+.grw-seen-user-info :global {
+  .btn.btn-seen-user {
+    $color-seen-user: #549c79;
+
+    @include bs.button-outline-variant($color-seen-user, $color-seen-user, rgba(lighten($color-seen-user, 10%), 0.15), rgba(lighten($color-seen-user, 10%), 0.5));
+    @include mixins.button-outline-svg-icon-variant($color-seen-user, $color-seen-user);
+
+    &:not(:disabled):not(.disabled):active,
+    &:not(:disabled):not(.disabled).active {
+      color: $color-seen-user;
+      svg {
+        fill: $color-seen-user;
+      }
+    }
+    &:not(:disabled):not(.disabled):not(:hover) {
+      background-color: transparent;
+    }
+  }
+}

+ 5 - 1
packages/app/src/components/User/SeenUserInfo.tsx

@@ -8,6 +8,10 @@ import { IUser } from '~/interfaces/user';
 
 import UserPictureList from './UserPictureList';
 
+
+import styles from './SeenUserInfo.module.scss';
+
+
 interface Props {
   seenUsers: IUser[],
   sumOfSeenUsers?: number,
@@ -23,7 +27,7 @@ const SeenUserInfo: FC<Props> = (props: Props) => {
   const togglePopover = () => setIsPopoverOpen(!isPopoverOpen);
 
   return (
-    <div className="grw-seen-user-info">
+    <div className={`grw-seen-user-info ${styles['grw-seen-user-info']}`}>
       <button type="button" id="btn-seen-user" className="shadow-none btn btn-seen-user border-0">
         <span className="mr-1 footstamp-icon">
           <FootstampIcon />

+ 26 - 6
packages/app/src/pages/login.page.tsx

@@ -4,9 +4,10 @@ import React from 'react';
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
-import dynamic from 'next/dynamic';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
+import { LoginForm } from '~/components/LoginForm';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 
 import {
@@ -16,11 +17,9 @@ import {
 
 
 import {
-  CommonProps, getServerSideCommonProps, useCustomTitle,
+  CommonProps, getServerSideCommonProps, useCustomTitle, getNextI18NextConfig,
 } from './utils/commons';
 
-const LoginForm = dynamic(() => import('~/components/LoginForm'), { ssr: false });
-
 type Props = CommonProps & {
 
   pageWithMetaStr: string,
@@ -41,12 +40,32 @@ const LoginPage: NextPage<Props> = (props: Props) => {
 
   return (
     <NoLoginLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')}>
-      <LoginForm objOfIsExternalAuthEnableds={props.enabledStrategies} isLocalStrategySetup={true} isLdapStrategySetup={true}
-        isRegistrationEnabled={true} registrationWhiteList={props.registrationWhiteList} isPasswordResetEnabled={true} />
+      <LoginForm
+        // Todo: These props should be set properly. https://redmine.weseek.co.jp/issues/104847
+        objOfIsExternalAuthEnableds={props.enabledStrategies}
+        isLocalStrategySetup={true}
+        isLdapStrategySetup={true}
+        isEmailAuthenticationEnabled={false}
+        isRegistrationEnabled={true}
+        registrationWhiteList={props.registrationWhiteList}
+        isPasswordResetEnabled={true}
+        isMailerSetup={false}
+      />
     </NoLoginLayout>
   );
 };
 
+/**
+ * for Server Side Translations
+ * @param context
+ * @param props
+ * @param namespacesRequired
+ */
+async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  props._nextI18Next = nextI18NextConfig._nextI18Next;
+}
+
 function injectEnabledStrategies(context: GetServerSidePropsContext, props: Props): void {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
@@ -92,6 +111,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
   injectServerConfigurations(context, props);
   injectEnabledStrategies(context, props);
+  await injectNextI18NextConfigurations(context, props, ['translation']);
 
   return {
     props,

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

@@ -169,7 +169,8 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 //  * @param namespacesRequired
 //  */
 async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  // preload all languages because of language lists in user setting
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired, true);
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 

+ 6 - 4
packages/app/src/pages/utils/commons.ts

@@ -1,4 +1,4 @@
-import { DevidedPagePath, Lang } from '@growi/core';
+import { DevidedPagePath, Lang, AllLang } from '@growi/core';
 import { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { SSRConfig, UserConfig } from 'next-i18next';
 
@@ -61,8 +61,10 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
 export const getNextI18NextConfig = async(
     // 'serverSideTranslations' method should be given from Next.js Page
     //  because importing it in this file causes https://github.com/isaachinman/next-i18next/issues/1545
-    serverSideTranslations: (initialLocale: string, namespacesRequired?: string[] | undefined, configOverride?: UserConfig | null) => Promise<SSRConfig>,
-    context: GetServerSidePropsContext, namespacesRequired?: string[] | undefined,
+    serverSideTranslations: (
+      initialLocale: string, namespacesRequired?: string[] | undefined, configOverride?: UserConfig | null, extraLocales?: string[] | false
+    ) => Promise<SSRConfig>,
+    context: GetServerSidePropsContext, namespacesRequired?: string[] | undefined, preloadAllLang = false,
 ): Promise<SSRConfig> => {
 
   const req: CrowiRequest = context.req as CrowiRequest;
@@ -74,7 +76,7 @@ export const getNextI18NextConfig = async(
     ?? configManager.getConfig('crowi', 'app:globalLang') as Lang
     ?? Lang.en_US;
 
-  return serverSideTranslations(locale, namespacesRequired ?? ['translation'], nextI18NextConfig);
+  return serverSideTranslations(locale, namespacesRequired ?? ['translation'], nextI18NextConfig, preloadAllLang ? AllLang : false);
 };
 
 /**

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

@@ -13,9 +13,9 @@ export const registerRules = () => {
     body('registerForm.name').not().isEmpty().withMessage('Name field is required'),
     body('registerForm.email')
       .isEmail()
-      .withMessage('Email format is invalid.')
+      .withMessage('Email format is invalid')
       .exists()
-      .withMessage('Email field is required.'),
+      .withMessage('Email field is required'),
     body('registerForm.password')
       .matches(/^[\x20-\x7F]*$/)
       .withMessage('Password has invalid character')

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

@@ -1,6 +1,8 @@
 import loggerFactory from '~/utils/logger';
 
+import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import injectUserRegistrationOrderByTokenMiddleware from '../../middlewares/inject-user-registration-order-by-token-middleware';
+import * as registerFormValidator from '../../middlewares/register-form-validator';
 
 import pageListing from './page-listing';
 import * as userActivation from './user-activation';
@@ -13,7 +15,7 @@ const router = express.Router();
 const routerForAdmin = express.Router();
 const routerForAuth = express.Router();
 
-module.exports = (crowi) => {
+module.exports = (crowi, app) => {
 
   // add custom functions to express response
   require('./response')(express, crowi);
@@ -38,8 +40,14 @@ module.exports = (crowi) => {
   routerForAdmin.use('/activity', require('./activity')(crowi));
 
   // auth
+  const applicationInstalled = require('../../middlewares/application-installed')(crowi);
+  const addActivity = generateAddActivityMiddleware(crowi);
+  const login = require('../login')(crowi, app);
+
   routerForAuth.use('/logout', require('./logout')(crowi));
 
+  routerForAuth.post('/register',
+    applicationInstalled, registerFormValidator.registerRules(), registerFormValidator.registerValidation, addActivity, login.register);
 
   router.use('/in-app-notification', require('./in-app-notification')(crowi));
 

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

@@ -61,9 +61,9 @@ module.exports = function(crowi, app) {
 
   /* eslint-disable max-len, comma-spacing, no-multi-spaces */
 
-  const [apiV3Router, apiV3AdminRouter, apiV3AuthRouter] = require('./apiv3')(crowi);
+  const [apiV3Router, apiV3AdminRouter, apiV3AuthRouter] = require('./apiv3')(crowi, app);
 
-  app.use('/api-docs', require('./apiv3/docs')(crowi));
+  app.use('/api-docs', require('./apiv3/docs')(crowi, app));
 
   // Rate limiter
   app.use(rateLimiter);
@@ -84,7 +84,6 @@ module.exports = function(crowi, app) {
   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.post('/register'                , applicationInstalled, registerFormValidator.registerRules(), registerFormValidator.registerValidation, csrfProtection, addActivity, login.register);
   app.get('/register'                 , applicationInstalled, login.preLogin, login.register);
 
   app.get('/admin/*'                    , applicationInstalled, loginRequiredStrictly , adminRequired , next.delegateToNext);

+ 73 - 85
packages/app/src/server/routes/login.js

@@ -19,22 +19,15 @@ module.exports = function(crowi, app) {
     req.login(userData, (err) => {
       if (err) {
         logger.debug(err);
-        // I created a flash message in case the user information that processing was successful is not stored in the session.
-        req.flash('successMessage', req.t('message.successfully_created', { username: userData.username }));
       }
       else {
         // update lastLoginAt
-        userData.updateLastLoginAt(new Date(), (err, userData) => {
+        userData.updateLastLoginAt(new Date(), (err) => {
           if (err) {
             logger.error(`updateLastLoginAt dumps error: ${err}`);
           }
         });
       }
-      // RegisterFormValidator.registerRule had code to guarantee that there was a password,
-      // but login.register did not. so I wrote this code.
-      if (!userData.password) {
-        return res.redirect('/me#password');
-      }
 
       const { redirectTo } = req.session;
       // remove session.redirectTo
@@ -43,10 +36,36 @@ module.exports = function(crowi, app) {
       const parameters = { action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS };
       activityEvent.emit('update', res.locals.activity._id, parameters);
 
-      return res.safeRedirect(redirectTo);
+      return res.apiv3({ redirectTo });
     });
   };
 
+  async function sendEmailToAllAdmins(userData) {
+    // send mails to all admin users (derived from crowi) -- 2020.06.18 Yuki Takei
+    const admins = await User.findAdmins();
+
+    const appTitle = appService.getAppTitle();
+
+    const promises = admins.map((admin) => {
+      return mailService.send({
+        to: admin.email,
+        subject: `[${appTitle}:admin] A New User Created and Waiting for Activation`,
+        template: path.join(crowi.localeDir, 'en_US/admin/userWaitingActivation.txt'),
+        vars: {
+          createdUser: userData,
+          admin,
+          url: appService.getSiteUrl(),
+          appTitle,
+        },
+      });
+    });
+
+    const results = await Promise.allSettled(promises);
+    results
+      .filter(result => result.status === 'rejected')
+      .forEach(result => logger.error(result.reason));
+  }
+
   actions.error = function(req, res) {
     const reason = req.params.reason;
 
@@ -93,97 +112,66 @@ module.exports = function(crowi, app) {
 
   actions.register = function(req, res) {
     if (req.user != null) {
-      return res.redirect('/');
+      return res.apiv3Err('user_already_logged_in', 403);
     }
 
     // config で closed ならさよなら
     if (configManager.getConfig('crowi', 'security:registrationMode') === aclService.labels.SECURITY_REGISTRATION_MODE_CLOSED) {
-      return res.redirect('/');
+      return res.apiv3Err('registration_closed', 403);
     }
 
-    if (req.method === 'POST' && req.form.isValid) {
-      const registerForm = req.form.registerForm || {};
-
-      const name = registerForm.name;
-      const username = registerForm.username;
-      const email = registerForm.email;
-      const password = registerForm.password;
-
-      // email と username の unique チェックする
-      User.isRegisterable(email, username, (isRegisterable, errOn) => {
-        let isError = false;
-        if (!User.isEmailValid(email)) {
-          isError = true;
-          req.flash('registerWarningMessage', req.t('message.email_address_could_not_be_used'));
-        }
-        if (!isRegisterable) {
-          if (!errOn.username) {
-            isError = true;
-            req.flash('registerWarningMessage', req.t('message.user_id_is_not_available'));
-          }
-          if (!errOn.email) {
-            isError = true;
-            req.flash('registerWarningMessage', req.t('message.email_address_is_already_registered'));
-          }
+    if (!req.form.isValid) {
+      const errors = req.form.errors;
+      return res.apiv3Err(errors, 401);
+    }
+
+    const registerForm = req.form.registerForm || {};
+
+    const name = registerForm.name;
+    const username = registerForm.username;
+    const email = registerForm.email;
+    const password = registerForm.password;
+
+    // email と username の unique チェックする
+    User.isRegisterable(email, username, (isRegisterable, errOn) => {
+      const errors = [];
+      if (!User.isEmailValid(email)) {
+        errors.push('email_address_could_not_be_used');
+      }
+      if (!isRegisterable) {
+        if (!errOn.username) {
+          errors.push('user_id_is_not_available');
         }
-        if (isError) {
-          debug('isError user register error', errOn);
-          return res.redirect('/register');
+        if (!errOn.email) {
+          errors.push('email_address_is_already_registered');
         }
+      }
+      if (errors.length > 0) {
+        debug('isError user register error', errOn);
+        return res.apiv3Err(errors, 400);
+      }
 
-        User.createUserByEmailAndPassword(name, username, email, password, undefined, async(err, userData) => {
-          if (err) {
-            if (err.name === 'UserUpperLimitException') {
-              req.flash('registerWarningMessage', req.t('message.can_not_register_maximum_number_of_users'));
-            }
-            else {
-              req.flash('registerWarningMessage', req.t('message.failed_to_register'));
-            }
-            return res.redirect('/register');
+      User.createUserByEmailAndPassword(name, username, email, password, undefined, async(err, userData) => {
+        if (err) {
+          const errors = [];
+          if (err.name === 'UserUpperLimitException') {
+            errors.push('can_not_register_maximum_number_of_users');
           }
-
-          if (configManager.getConfig('crowi', 'security:registrationMode') !== aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
-            // send mail asynchronous
-            sendEmailToAllAdmins(userData);
+          else {
+            errors.push('failed_to_register');
           }
+          return res.apiv3Err(errors, 405);
+        }
 
+        if (configManager.getConfig('crowi', 'security:registrationMode') !== aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
+          // send mail asynchronous
+          sendEmailToAllAdmins(userData);
+        }
 
-          return registerSuccessHandler(req, res, userData);
-        });
-      });
-    }
-    else { // method GET of form is not valid
-      debug('session is', req.session);
-      const isRegistering = true;
-      return res.render('login', { isRegistering });
-    }
-  };
-
-  async function sendEmailToAllAdmins(userData) {
-    // send mails to all admin users (derived from crowi) -- 2020.06.18 Yuki Takei
-    const admins = await User.findAdmins();
-
-    const appTitle = appService.getAppTitle();
-
-    const promises = admins.map((admin) => {
-      return mailService.send({
-        to: admin.email,
-        subject: `[${appTitle}:admin] A New User Created and Waiting for Activation`,
-        template: path.join(crowi.localeDir, 'en_US/admin/userWaitingActivation.txt'),
-        vars: {
-          createdUser: userData,
-          admin,
-          url: appService.getSiteUrl(),
-          appTitle,
-        },
+        return registerSuccessHandler(req, res, userData);
       });
     });
-
-    const results = await Promise.allSettled(promises);
-    results
-      .filter(result => result.status === 'rejected')
-      .forEach(result => logger.error(result.reason));
-  }
+  };
 
   actions.invited = async function(req, res) {
     if (!req.user) {

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

@@ -1,23 +1,6 @@
 @use '../bootstrap/init' as bs;
 @use '../mixins';
 
-.btn.btn-seen-user {
-  $color-seen-user: #549c79;
-
-  @include bs.button-outline-variant($color-seen-user, $color-seen-user, rgba(lighten($color-seen-user, 10%), 0.15), rgba(lighten($color-seen-user, 10%), 0.5));
-  @include mixins.button-outline-svg-icon-variant($color-seen-user, $color-seen-user);
-  &:not(:disabled):not(.disabled):active,
-  &:not(:disabled):not(.disabled).active {
-    color: $color-seen-user;
-    svg {
-      fill: $color-seen-user;
-    }
-  }
-  &:not(:disabled):not(.disabled):not(:hover) {
-    background-color: transparent;
-  }
-}
-
 .btn-copy,
 .btn-edit {
   &:not(:hover):not(:active) {

+ 1 - 1
packages/app/src/styles/theme/_apply-colors.scss

@@ -169,7 +169,7 @@ ul.pagination {
 
 .grw-navbar {
   background: $bgcolor-navbar;
-  .nav-item > .nav-link {
+  .nav-item .nav-link {
     color: $color-link-nabvar;
   }
 

+ 31 - 30
yarn.lock

@@ -1441,13 +1441,6 @@
   dependencies:
     regenerator-runtime "^0.13.4"
 
-"@babel/runtime@^7.13.17", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2":
-  version "7.18.6"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580"
-  integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==
-  dependencies:
-    regenerator-runtime "^0.13.4"
-
 "@babel/runtime@^7.13.8", "@babel/runtime@^7.8.7":
   version "7.17.7"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.7.tgz#a5f3328dc41ff39d803f311cfe17703418cf9825"
@@ -1455,6 +1448,13 @@
   dependencies:
     regenerator-runtime "^0.13.4"
 
+"@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580"
+  integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
 "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa"
@@ -1462,6 +1462,13 @@
   dependencies:
     regenerator-runtime "^0.13.4"
 
+"@babel/runtime@^7.18.6":
+  version "7.19.0"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
+  integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
 "@babel/runtime@^7.6.3":
   version "7.7.7"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.7.tgz#194769ca8d6d7790ec23605af9ee3e42a0aa79cf"
@@ -11582,7 +11589,7 @@ hogan.js@3.0.2:
     mkdirp "0.3.0"
     nopt "1.0.10"
 
-hoist-non-react-statics@^3.2.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
+hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
   integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -11617,11 +11624,6 @@ html-escaper@^2.0.0:
   resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.0.tgz#71e87f931de3fe09e56661ab9a29aadec707b491"
   integrity sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==
 
-html-escaper@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
-  integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
-
 html-parse-stringify@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
@@ -11807,10 +11809,10 @@ i18next-localstorage-backend@^3.1.3:
   dependencies:
     "@babel/runtime" "^7.14.6"
 
-i18next@^21.6.14:
-  version "21.8.11"
-  resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.8.11.tgz#197aac04be51b7083999a2cb63deb831cf1ba4c8"
-  integrity sha512-+s8N6kQShwNK+Ua/+VsS/Sji24NUJJLBk9QIucygj1f97f4hPNDWmLP9fQCI4d5+XLfXJ3JctX4g+zJla967Vw==
+i18next@^21.8.13:
+  version "21.9.2"
+  resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.9.2.tgz#3f7c5594393eb27117c1db4c38f5ec766e68de0e"
+  integrity sha512-00fVrLQOwy45nm3OtC9l1WiLK3nJlIYSljgCt0qzTaAy65aciMdRy9GsuW+a2AtKtdg9/njUGfRH30LRupV7ZQ==
   dependencies:
     "@babel/runtime" "^7.17.2"
 
@@ -16108,18 +16110,18 @@ nested-error-stacks@^2.0.0:
   resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61"
   integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==
 
-next-i18next@^11.0.0:
-  version "11.0.0"
-  resolved "https://registry.yarnpkg.com/next-i18next/-/next-i18next-11.0.0.tgz#2857d13c58a5ed976fe57c44286f1520b07f7c96"
-  integrity sha512-phxbQiZGSJTTBE2FI4+BnqFZl88AI2V+6MrEQnT9aPFAXq/fATQ/F0pOUM3J7kU4nEeCfn3hjISq+ygGHlEz0g==
+next-i18next@^11.3.0:
+  version "11.3.0"
+  resolved "https://registry.yarnpkg.com/next-i18next/-/next-i18next-11.3.0.tgz#bfce51d8df07fb5cd61097423eeb7d744e09ae25"
+  integrity sha512-xl0oIRtiVrk9ZaWBRUbNk/prva4Htdu59o9rFWzd9ax/KemaDVuTTuBZTQMkmXohUQk/MJ7w1rV/mICL6TzyGw==
   dependencies:
-    "@babel/runtime" "^7.13.17"
+    "@babel/runtime" "^7.18.6"
     "@types/hoist-non-react-statics" "^3.3.1"
     core-js "^3"
-    hoist-non-react-statics "^3.2.0"
-    i18next "^21.6.14"
+    hoist-non-react-statics "^3.3.2"
+    i18next "^21.8.13"
     i18next-fs-backend "^1.1.4"
-    react-i18next "^11.16.2"
+    react-i18next "^11.18.0"
 
 next-superjson@^0.0.4:
   version "0.0.4"
@@ -18450,13 +18452,12 @@ react-hotkeys@^2.0.0:
   dependencies:
     prop-types "^15.6.1"
 
-react-i18next@^11.16.2:
-  version "11.17.3"
-  resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.17.3.tgz#eff742f162f1a413056fb510e8b830d42c8c020d"
-  integrity sha512-rIrLl5cLDoHdXFWdjKurRpatA3MPC9j3yTZidv0GmJEea5+XGXl42p7NupA1dmghoLGOXllShNUobgPYtgEcRA==
+react-i18next@^11.18.0:
+  version "11.18.6"
+  resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.18.6.tgz#e159c2960c718c1314f1e8fcaa282d1c8b167887"
+  integrity sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==
   dependencies:
     "@babel/runtime" "^7.14.5"
-    html-escaper "^2.0.2"
     html-parse-stringify "^3.0.1"
 
 react-image-crop@^8.3.0: