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

Merge branch 'master' into feat/106973-enable-View-this-version

ryoji-s 3 лет назад
Родитель
Сommit
ec3fcde842

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

@@ -661,6 +661,7 @@
     "email_address_is_already_registered":"This email address is already registered.",
     "can_not_register_maximum_number_of_users":"Can not register more than the maximum number of users.",
     "email_settings_is_not_setup":"E-mail settings is not set up. Please ask the administrator.",
+    "email_authentication_is_not_enabled": "Email authentication is not enabled. Please ask the administrator.",
     "failed_to_register":"Failed to register.",
     "successfully_created":"The user {{username}} is successfully created.",
     "can_not_activate_maximum_number_of_users":"Can not activate more than the maximum number of users.",

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

@@ -655,6 +655,7 @@
     "email_address_is_already_registered":"このメールアドレスは既に登録されています。",
     "can_not_register_maximum_number_of_users":"ユーザー数が上限を超えたため登録できません。",
     "email_settings_is_not_setup":"E-mail 設定が完了していません。管理者に問い合わせてください。",
+    "email_authentication_is_not_enabled": "メール認証が有効になっていません。管理者に問い合わせてください。",
     "failed_to_register":"登録に失敗しました。",
     "successfully_created":"{{username}} が作成されました。",
     "can_not_activate_maximum_number_of_users":"ユーザーが上限に達したためアクティベートできません。",

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

@@ -663,6 +663,7 @@
 		"email_address_is_already_registered": "此电子邮件地址已注册。",
 		"can_not_register_maximum_number_of_users": "注册的用户数不能超过最大值。",
     "email_settings_is_not_setup":"邮箱设置未设置,请询问管理员。",
+    "email_authentication_is_not_enabled": "电子邮件验证未被激活, 请询问管理员。",
 		"failed_to_register": "注册失败。",
 		"successfully_created": "已成功创建用户{{username}。",
 		"can_not_activate_maximum_number_of_users": "无法激活超过最大用户数的用户。",

+ 113 - 87
packages/app/src/components/CompleteUserRegistrationForm.tsx

@@ -1,30 +1,36 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
+
 import { useTranslation } from 'next-i18next';
+
 import { apiv3Get, apiv3Post } from '~/client/util/apiv3-client';
+import { UserActivationErrorCode } from '~/interfaces/errors/user-activation';
 
 import { toastSuccess, toastError } from '../client/util/apiNotification';
 
 interface Props {
-  messageErrors?: any,
-  inputs?: any,
   email: string,
   token: string,
+  errorCode?: UserActivationErrorCode,
+  isEmailAuthenticationEnabled: boolean,
 }
 
 const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
 
   const { t } = useTranslation();
   const {
-    messageErrors,
     email,
     token,
+    errorCode,
+    isEmailAuthenticationEnabled,
   } = props;
 
+  const forceDisableForm = errorCode != null || !isEmailAuthenticationEnabled;
+
   const [usernameAvailable, setUsernameAvailable] = useState(true);
   const [username, setUsername] = useState('');
   const [name, setName] = useState('');
   const [password, setPassword] = useState('');
-  const [disableForm, setDisableForm] = useState(false);
+  const [disableForm, setDisableForm] = useState(forceDisableForm);
 
   useEffect(() => {
     const delayDebounceFn = setTimeout(async() => {
@@ -42,7 +48,8 @@ const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
     return () => clearTimeout(delayDebounceFn);
   }, [username]);
 
-  async function submitRegistration() {
+  const handleSubmitRegistration = useCallback(async(e) => {
+    e.preventDefault();
     setDisableForm(true);
     try {
       await apiv3Post('/complete-registration', {
@@ -55,91 +62,110 @@ const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
       toastError(err, 'Registration failed');
       setDisableForm(false);
     }
-  }
+  }, [name, password, token, username]);
 
   return (
     <>
-      <div id="register-form-errors">
-        {messageErrors && (
-          <div className="alert alert-danger">
-            { messageErrors }
-          </div>
-        )}
-      </div>
-      <div id="register-dialog">
-
-        <fieldset id="registration-form" disabled={disableForm}>
-          <input type="hidden" name="token" value={token} />
-          <div className="input-group">
-            <div className="input-group-prepend">
-              <span className="input-group-text"><i className="icon-envelope"></i></span>
-            </div>
-            <input type="text" className="form-control" disabled value={email} />
-          </div>
-          <div className="input-group" id="input-group-username">
-            <div className="input-group-prepend">
-              <span className="input-group-text"><i className="icon-user"></i></span>
-            </div>
-            <input
-              type="text"
-              className="form-control"
-              placeholder={t('User ID')}
-              name="username"
-              onChange={e => setUsername(e.target.value)}
-              required
-            />
-          </div>
-          {!usernameAvailable && (
-            <p className="form-text text-red">
-              <span id="help-block-username"><i className="icon-fw icon-ban"></i>{t('installer.unavaliable_user_id')}</span>
-            </p>
-          )}
-
-          <div className="input-group">
-            <div className="input-group-prepend">
-              <span className="input-group-text"><i className="icon-tag"></i></span>
-            </div>
-            <input
-              type="text"
-              className="form-control"
-              placeholder={t('Name')}
-              name="name"
-              value={name}
-              onChange={e => setName(e.target.value)}
-              required
-            />
+      <div className="noLogin-dialog mx-auto" id="noLogin-dialog">
+        <div className="row mx-0">
+          <div className="col-12">
+
+            { (errorCode != null && errorCode === UserActivationErrorCode.TOKEN_NOT_FOUND) && (
+              <p className="alert alert-danger">
+                <span>Token not found</span>
+              </p>
+            )}
+
+            { (errorCode != null && errorCode === UserActivationErrorCode.USER_REGISTRATION_ORDER_IS_NOT_APPROPRIATE) && (
+              <p className="alert alert-danger">
+                <span>{t('message.incorrect_token_or_expired_url')}</span>
+              </p>
+            )}
+
+            { !isEmailAuthenticationEnabled && (
+              <p className="alert alert-danger">
+                <span>{t('message.email_authentication_is_not_enabled')}</span>
+              </p>
+            )}
+
+            <form role="form" onSubmit={handleSubmitRegistration} id="registration-form">
+              <input type="hidden" name="token" value={token} />
+
+              <div className="input-group">
+                <div className="input-group-prepend">
+                  <span className="input-group-text"><i className="icon-envelope"></i></span>
+                </div>
+                <input type="text" className="form-control" placeholder={t('Email')} disabled value={email} />
+              </div>
+
+              <div className="input-group" id="input-group-username">
+                <div className="input-group-prepend">
+                  <span className="input-group-text"><i className="icon-user"></i></span>
+                </div>
+                <input
+                  type="text"
+                  className="form-control"
+                  placeholder={t('User ID')}
+                  name="username"
+                  onChange={e => setUsername(e.target.value)}
+                  required
+                  disabled={forceDisableForm || disableForm}
+                />
+              </div>
+              {!usernameAvailable && (
+                <p className="form-text text-red">
+                  <span id="help-block-username"><i className="icon-fw icon-ban"></i>{t('installer.unavaliable_user_id')}</span>
+                </p>
+              )}
+
+              <div className="input-group">
+                <div className="input-group-prepend">
+                  <span className="input-group-text"><i className="icon-tag"></i></span>
+                </div>
+                <input
+                  type="text"
+                  className="form-control"
+                  placeholder={t('Name')}
+                  name="name"
+                  value={name}
+                  onChange={e => setName(e.target.value)}
+                  required
+                  disabled={forceDisableForm || disableForm}
+                />
+              </div>
+
+              <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"
+                  placeholder={t('Password')}
+                  name="password"
+                  value={password}
+                  onChange={e => setPassword(e.target.value)}
+                  required
+                  disabled={forceDisableForm || disableForm}
+                />
+              </div>
+
+              <div className="input-group justify-content-center d-flex mt-5">
+                <button disabled={forceDisableForm || disableForm} className="btn btn-fill" id="register">
+                  <div className="eff"></div>
+                  <span className="btn-label"><i className="icon-user-follow"></i></span>
+                  <span className="btn-label-text">{t('Create')}</span>
+                </button>
+              </div>
+
+              <div className="input-group mt-5 d-flex justify-content-center">
+                <a href="https://growi.org" className="link-growi-org">
+                  <span className="growi">GROWI</span>.<span className="org">ORG</span>
+                </a>
+              </div>
+            </form>
           </div>
-
-          <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"
-              placeholder={t('Password')}
-              name="password"
-              value={password}
-              onChange={e => setPassword(e.target.value)}
-              required
-            />
-          </div>
-
-          <div className="input-group justify-content-center d-flex mt-5">
-            <button type="button" onClick={submitRegistration} className="btn btn-fill" id="register">
-              <div className="eff"></div>
-              <span className="btn-label"><i className="icon-user-follow"></i></span>
-              <span className="btn-label-text">{t('Create')}</span>
-            </button>
-          </div>
-
-          <div className="input-group mt-5 d-flex justify-content-center">
-            <a href="https://growi.org" className="link-growi-org">
-              <span className="growi">GROWI</span>.<span className="org">ORG</span>
-            </a>
-          </div>
-
-        </fieldset>
+        </div>
       </div>
     </>
   );

+ 6 - 0
packages/app/src/interfaces/errors/user-activation.ts

@@ -0,0 +1,6 @@
+export const UserActivationErrorCode = {
+  TOKEN_NOT_FOUND: 'token-not-found',
+  USER_REGISTRATION_ORDER_IS_NOT_APPROPRIATE: 'user-registration-order-is-not-appropriate',
+} as const;
+
+export type UserActivationErrorCode = typeof UserActivationErrorCode[keyof typeof UserActivationErrorCode];

+ 76 - 0
packages/app/src/pages/user-activation.page.tsx

@@ -0,0 +1,76 @@
+import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+
+import CompleteUserRegistrationForm from '~/components/CompleteUserRegistrationForm';
+import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
+import type { UserActivationErrorCode } from '~/interfaces/errors/user-activation';
+import { IUserRegistrationOrder } from '~/server/models/user-registration-order';
+
+import {
+  getServerSideCommonProps, getNextI18NextConfig, useCustomTitle, CommonProps,
+} from './utils/commons';
+
+type Props = CommonProps & {
+  token: string
+  email: string
+  errorCode?: UserActivationErrorCode
+  isEmailAuthenticationEnabled: boolean
+}
+
+const UserActivationPage: NextPage<Props> = (props: Props) => {
+  return (
+    <NoLoginLayout title={useCustomTitle(props, 'GROWI')}>
+      <CompleteUserRegistrationForm
+        token={props.token}
+        email={props.email}
+        errorCode={props.errorCode}
+        isEmailAuthenticationEnabled={props.isEmailAuthenticationEnabled}
+      />
+    </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;
+}
+
+export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+  const result = await getServerSideCommonProps(context);
+  const req: CrowiRequest = context.req as CrowiRequest;
+
+  // check for presence
+  // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
+  if (!('props' in result)) {
+    throw new Error('invalid getSSP result');
+  }
+
+  const props: Props = result.props as Props;
+
+  if (context.query.userRegistrationOrder != null) {
+    const userRegistrationOrder = context.query.userRegistrationOrder as unknown as IUserRegistrationOrder;
+    props.email = userRegistrationOrder.email;
+    props.token = userRegistrationOrder.token;
+  }
+
+  if (typeof context.query.errorCode === 'string') {
+    props.errorCode = context.query.errorCode as UserActivationErrorCode;
+  }
+
+  props.isEmailAuthenticationEnabled = req.crowi.configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled');
+
+  await injectNextI18NextConfigurations(context, props, ['translation']);
+
+  return {
+    props,
+  };
+};
+
+export default UserActivationPage;

+ 19 - 4
packages/app/src/server/middlewares/inject-user-registration-order-by-token-middleware.ts

@@ -1,19 +1,34 @@
+import { Request, Response, NextFunction } from 'express';
 import createError from 'http-errors';
 
-import UserRegistrationOrder from '../models/user-registration-order';
+import { UserActivationErrorCode } from '~/interfaces/errors/user-activation';
+import loggerFactory from '~/utils/logger';
 
-export default async(req, res, next): Promise<void> => {
+import UserRegistrationOrder, { IUserRegistrationOrder } from '../models/user-registration-order';
+
+const logger = loggerFactory('growi:routes:user-activation');
+
+export type ReqWithUserRegistrationOrder = Request & {
+  userRegistrationOrder: IUserRegistrationOrder
+};
+
+// eslint-disable-next-line import/no-anonymous-default-export
+export default async(req: ReqWithUserRegistrationOrder, res: Response, next: NextFunction): Promise<void> => {
   const token = req.params.token || req.body.token;
 
   if (token == null) {
-    return next(createError(400, 'Token not found', { code: 'token-not-found' }));
+    const msg = 'Token not found';
+    logger.error(msg);
+    return next(createError(400, msg, { code: UserActivationErrorCode.TOKEN_NOT_FOUND }));
   }
 
   const userRegistrationOrder = await UserRegistrationOrder.findOne({ token });
 
   // check if the token is valid
   if (userRegistrationOrder == null || userRegistrationOrder.isExpired() || userRegistrationOrder.isRevoked) {
-    return next(createError(400, 'userRegistrationOrder is null or expired or revoked', { code: 'password-reset-order-is-not-appropriate' }));
+    const msg = 'userRegistrationOrder is null or expired or revoked';
+    logger.error(msg);
+    return next(createError(400, msg, { code: UserActivationErrorCode.USER_REGISTRATION_ORDER_IS_NOT_APPROPRIATE }));
   }
 
   req.userRegistrationOrder = userRegistrationOrder;

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

@@ -147,7 +147,6 @@ export const completeRegistrationAction = (crowi) => {
           }
         }
 
-        req.flash('successMessage', req.t('message.successfully_created', { username }));
         res.apiv3({ status: 'ok' });
       });
     });

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

@@ -198,9 +198,10 @@ module.exports = function(crowi, app) {
     .use(forgotPassword.handleErrorsMiddleware(crowi)));
 
   app.get('/_private-legacy-pages', next.delegateToNext);
+
   app.use('/user-activation', express.Router()
-    .get('/:token', applicationInstalled, injectUserRegistrationOrderByTokenMiddleware, userActivation.form)
-    .use(userActivation.tokenErrorHandlerMiddeware));
+    .get('/:token', applicationInstalled, injectUserRegistrationOrderByTokenMiddleware, userActivation.renderUserActivationPage(crowi))
+    .use(userActivation.tokenErrorHandlerMiddeware(crowi)));
 
   app.get('/share/:linkId', next.delegateToNext);
 

+ 33 - 9
packages/app/src/server/routes/user-activation.ts

@@ -1,13 +1,37 @@
-export const form = (req, res): void => {
-  const { userRegistrationOrder } = req;
-  return res.render('user-activation', { userRegistrationOrder });
+import { Response, NextFunction } from 'express';
+
+import type { UserActivationErrorCode } from '~/interfaces/errors/user-activation';
+import { ReqWithUserRegistrationOrder } from '~/server/middlewares/inject-user-registration-order-by-token-middleware';
+
+type Crowi = {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  nextApp: any,
+}
+
+type CrowiReq = ReqWithUserRegistrationOrder & {
+  crowi: Crowi,
+}
+
+export const renderUserActivationPage = (crowi: Crowi) => {
+  return (req: CrowiReq, res: Response): void => {
+    const { userRegistrationOrder } = req;
+    const { nextApp } = crowi;
+    req.crowi = crowi;
+    nextApp.render(req, res, '/user-activation', { userRegistrationOrder });
+    return;
+  };
 };
 
 // middleware to handle error
-export const tokenErrorHandlerMiddeware = (err, req, res, next) => {
-  if (err != null) {
-    req.flash('errorMessage', req.t('message.incorrect_token_or_expired_url'));
-    return res.redirect('/login#register');
-  }
-  next();
+export const tokenErrorHandlerMiddeware = (crowi: Crowi) => {
+  return (error: Error & { code: UserActivationErrorCode, statusCode: number }, req: CrowiReq, res: Response, next: NextFunction): void => {
+    if (error != null) {
+      const { nextApp } = crowi;
+      req.crowi = crowi;
+      nextApp.render(req, res, '/user-activation', { errorCode: error.code });
+      return;
+    }
+
+    next();
+  };
 };