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

Merge pull request #6859 from weseek/feat/107958/enable-email-sending-for-email-authentication

feat: Enable email sending for email authentication
Yuki Takei 3 лет назад
Родитель
Сommit
937fb395f3

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

@@ -105,7 +105,6 @@
       "email_authentication": "Email authentication on user registration",
       "enable_email_authentication": "Enable email authentication",
       "enable_email_authentication_desc": "Email authentication is going to be performed for user registration.",
-      "please_enable_mailer": "Please setup mailer first.",
       "need_complete_mail_setting_warning": "To use the following functions, please complete the mail settings."
     },
     "ldap": {

+ 2 - 1
packages/app/public/static/locales/en_US/commons.json

@@ -12,7 +12,8 @@
     "remove_share_link": "Succeeded to remove {{count}} share links"
   },
   "alert": {
-    "siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}"
+    "siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
+    "please_enable_mailer": "Please setup mailer first."
   },
   "headers": {
     "app_settings": "App Settings"

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

@@ -112,7 +112,6 @@
       "email_authentication": "ユーザー登録時のメール認証",
       "enable_email_authentication": "メール認証を有効にする",
       "enable_email_authentication_desc": "ユーザー登録時にメール認証を行います。",
-      "please_enable_mailer": "メール認証を有効にするには、メール設定を完了させてください。",
       "need_complete_mail_setting_warning": "以下の機能を使えるようにするには、メール設定を完了させてください。"
     },
     "ldap": {

+ 2 - 1
packages/app/public/static/locales/ja_JP/commons.json

@@ -12,7 +12,8 @@
     "remove_share_link": "共有リンクを{{count}}件削除しました"
   },
   "alert": {
-    "siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。"
+    "siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",
+    "please_enable_mailer": "メール認証を有効にするには、メール設定を完了させてください。"
   },
   "headers": {
     "app_settings": "アプリ設定"

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

@@ -114,7 +114,6 @@
       "email_authentication": "用户注册时的电子邮件身份验证",
       "enable_email_authentication": "启用电子邮件身份验证",
       "enable_email_authentication_desc": "用户注册将执行电子邮件身份验证。",
-      "please_enable_mailer": "请先设置邮件程序。",
       "need_complete_mail_setting_warning": "要使用以下功能,请完成邮件设置。"
 		},
 		"ldap": {

+ 2 - 1
packages/app/public/static/locales/zh_CN/commons.json

@@ -12,7 +12,8 @@
     "remove_share_link": "Succeeded to remove {{count}} share links"
   },
   "alert": {
-    "siteUrl_is_not_set": "主页URL未设置,通过 {{link}} 设置"
+    "siteUrl_is_not_set": "主页URL未设置,通过 {{link}} 设置",
+    "please_enable_mailer": "请先设置邮件程序。"
   },
   "headers": {
     "app_settings": "系统设置"

+ 1 - 1
packages/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx

@@ -212,7 +212,7 @@ class LocalSecuritySettingContents extends React.Component {
                 </div>
                 {!isMailerSetup && (
                   <div className="alert alert-warning p-1 my-1 small d-inline-block">
-                    <span>{t('security_settings.Local.please_enable_mailer')}</span>
+                    <span>{t('commons:alert.please_enable_mailer')}</span>
                     <a href="/admin/app#mail-settings"> <i className="fa fa-link"></i> {t('app_setting.mail_settings')}</a>
                   </div>
                 )}

+ 27 - 5
packages/app/src/components/LoginForm.tsx

@@ -49,6 +49,10 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   const [emailForRegister, setEmailForRegister] = useState('');
   const [passwordForRegister, setPasswordForRegister] = useState('');
   const [registerErrors, setRegisterErrors] = useState<IErrorV3[]>([]);
+  // For UserActivation
+  const [emailForRegistrationOrder, setEmailForRegistrationOrder] = useState('');
+  const [isSuccessToSendRegistrationOrderEmail, setIsSuccessToSendRegistrationOrderEmail] = useState(false);
+
 
   useEffect(() => {
     const { hash } = window.location;
@@ -261,6 +265,8 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
   const handleRegisterFormSubmit = useCallback(async(e, requestPath) => {
     e.preventDefault();
+    setEmailForRegistrationOrder('');
+    setIsSuccessToSendRegistrationOrderEmail(false);
 
     const registerForm = {
       username: usernameForRegister,
@@ -272,6 +278,11 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       const res = await apiv3Post(requestPath, { registerForm });
       const { redirectTo } = res.data;
       router.push(redirectTo ?? '/');
+
+      if (isEmailAuthenticationEnabled) {
+        setEmailForRegistrationOrder(emailForRegister);
+        setIsSuccessToSendRegistrationOrderEmail(true);
+      }
     }
     catch (err) {
       // Execute if error exists
@@ -280,7 +291,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       }
     }
     return;
-  }, [emailForRegister, nameForRegister, passwordForRegister, router, usernameForRegister]);
+  }, [emailForRegister, nameForRegister, passwordForRegister, router, usernameForRegister, isEmailAuthenticationEnabled]);
 
   const resetRegisterErrors = useCallback(() => {
     if (registerErrors.length === 0) return;
@@ -313,7 +324,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
         )}
         { (!isMailerSetup && isEmailAuthenticationEnabled) && (
           <p className="alert alert-danger">
-            <span>{t('security_settings.Local.please_enable_mailer')}</span>
+            <span>{t('commons:alert.please_enable_mailer')}</span>
           </p>
         )}
 
@@ -331,6 +342,14 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
           )
         }
 
+        {
+          (isEmailAuthenticationEnabled && isSuccessToSendRegistrationOrderEmail) && (
+            <p className="alert alert-success">
+              <span>{t('message.successfully_send_email_auth', { email: emailForRegistrationOrder })}</span>
+            </p>
+          )
+        }
+
         <form role="form" onSubmit={e => handleRegisterFormSubmit(e, registerAction) } id="register-form">
 
           {!isEmailAuthenticationEnabled && (
@@ -381,6 +400,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             </div>
             {/* email */}
             <input type="email"
+              disabled={!isMailerSetup && isEmailAuthenticationEnabled}
               className="form-control rounded-0"
               onChange={(e) => { setEmailForRegister(e.target.value) }}
               placeholder={t('Email')}
@@ -452,9 +472,11 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
         </div>
       </React.Fragment>
     );
-  }, [handleRegisterFormSubmit, isEmailAuthenticationEnabled, isMailerSetup,
-      props.email, props.name, props.username,
-      registerErrors, registrationMode, registrationWhiteList, switchForm, t]);
+  }, [
+    handleRegisterFormSubmit, isEmailAuthenticationEnabled, isMailerSetup,
+    isSuccessToSendRegistrationOrderEmail, props.email, props.name, props.username,
+    registerErrors, registrationMode, registrationWhiteList, emailForRegistrationOrder, switchForm, t,
+  ]);
 
   return (
     <div className="noLogin-dialog mx-auto" id="noLogin-dialog">

+ 1 - 1
packages/app/src/pages/login.page.tsx

@@ -54,7 +54,7 @@ const LoginPage: NextPage<Props> = (props: Props) => {
         isRegistrationEnabled={true}
         registrationWhiteList={props.registrationWhiteList}
         isPasswordResetEnabled={true}
-        isMailerSetup={true}
+        isMailerSetup={props.isMailerSetup}
       />
     </NoLoginLayout>
   );

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

@@ -55,6 +55,8 @@ module.exports = (crowi, app, isInstalled) => {
   routerForAuth.post('/register',
     applicationInstalled, registerFormValidator.registerRules(), registerFormValidator.registerValidation, addActivity, login.register);
 
+  routerForAuth.post('/user-activation/register', applicationInstalled, userActivation.registerRules(),
+    userActivation.validateRegisterForm, userActivation.registerAction(crowi));
 
   // installer
   if (!isInstalled) {

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

@@ -1,8 +1,10 @@
 import path from 'path';
 
 import { ErrorV3 } from '@growi/core';
+import { format, subSeconds } from 'date-fns';
 import { body, validationResult } from 'express-validator';
 
+import UserRegistrationOrder from '~/server/models/user-registration-order';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:routes:apiv3:user-activation');
@@ -151,3 +153,89 @@ export const completeRegistrationAction = (crowi) => {
     });
   };
 };
+
+// validation rules for registration form when email authentication enabled
+export const registerRules = () => {
+  return [
+    body('registerForm.email')
+      .isEmail()
+      .withMessage('Email format is invalid.')
+      .exists()
+      .withMessage('Email field is required.'),
+  ];
+};
+
+// middleware to validate register form if email authentication enabled
+export const validateRegisterForm = (req, res, next) => {
+  const errors = validationResult(req);
+  if (errors.isEmpty()) {
+    return next();
+  }
+
+  const extractedErrors: string[] = [];
+  errors.array().map(err => extractedErrors.push(err.msg));
+
+  return res.apiv3Err(extractedErrors, 400);
+};
+
+async function makeRegistrationEmailToken(email, crowi) {
+  const {
+    configManager,
+    mailService,
+    localeDir,
+    appService,
+  } = crowi;
+
+  const isMailerSetup = mailService.isMailerSetup ?? false;
+  if (!isMailerSetup) {
+    throw Error('mailService is not setup');
+  }
+
+  const grobalLang = configManager.getConfig('crowi', 'app:globalLang');
+  const i18n = grobalLang;
+  const appUrl = appService.getSiteUrl();
+
+  const userRegistrationOrder = await UserRegistrationOrder.createUserRegistrationOrder(email);
+  const grwTzoffsetSec = crowi.appService.getTzoffset() * 60;
+  const expiredAt = subSeconds(userRegistrationOrder.expiredAt, grwTzoffsetSec);
+  const formattedExpiredAt = format(expiredAt, 'yyyy/MM/dd HH:mm');
+  const url = new URL(`/user-activation/${userRegistrationOrder.token}`, appUrl);
+  const oneTimeUrl = url.href;
+  const txtFileName = 'userActivation';
+
+  return mailService.send({
+    to: email,
+    subject: '[GROWI] User Activation',
+    template: path.join(localeDir, `${i18n}/notifications/${txtFileName}.txt`),
+    vars: {
+      appTitle: appService.getAppTitle(),
+      email,
+      expiredAt: formattedExpiredAt,
+      url: oneTimeUrl,
+    },
+  });
+}
+
+export const registerAction = (crowi) => {
+  const User = crowi.model('User');
+
+  return async function(req, res) {
+    const registerForm = req.body.registerForm || {};
+    const email = registerForm.email;
+    const isRegisterableEmail = await User.isRegisterableEmail(email);
+
+    if (!isRegisterableEmail) {
+      req.body.registerForm.email = email;
+      return res.apiv3Err(['message.email_address_is_already_registered'], 400);
+    }
+
+    try {
+      await makeRegistrationEmailToken(email, crowi);
+    }
+    catch (err) {
+      return res.apiv3Err(err);
+    }
+
+    return res.apiv3({ redirectTo: '/login#register' });
+  };
+};

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

@@ -201,7 +201,6 @@ module.exports = function(crowi, app) {
   app.use('/user-activation', express.Router()
     .get('/:token', applicationInstalled, injectUserRegistrationOrderByTokenMiddleware, userActivation.form)
     .use(userActivation.tokenErrorHandlerMiddeware));
-  app.post('/user-activation/register', applicationInstalled, csrfProtection, userActivation.registerRules(), userActivation.validateRegisterForm, userActivation.registerAction(crowi));
 
   app.get('/share/:linkId', next.delegateToNext);
 

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

@@ -1,72 +1,8 @@
-import path from 'path';
-
-import { format, subSeconds } from 'date-fns';
-import { body, validationResult } from 'express-validator';
-
-import UserRegistrationOrder from '../models/user-registration-order';
-
 export const form = (req, res): void => {
   const { userRegistrationOrder } = req;
   return res.render('user-activation', { userRegistrationOrder });
 };
 
-async function makeRegistrationEmailToken(email, crowi) {
-  const {
-    configManager,
-    mailService,
-    localeDir,
-    appService,
-  } = crowi;
-
-  const grobalLang = configManager.getConfig('crowi', 'app:globalLang');
-  const i18n = grobalLang;
-  const appUrl = appService.getSiteUrl();
-
-  const userRegistrationOrder = await UserRegistrationOrder.createUserRegistrationOrder(email);
-  const grwTzoffsetSec = crowi.appService.getTzoffset() * 60;
-  const expiredAt = subSeconds(userRegistrationOrder.expiredAt, grwTzoffsetSec);
-  const formattedExpiredAt = format(expiredAt, 'yyyy/MM/dd HH:mm');
-  const url = new URL(`/user-activation/${userRegistrationOrder.token}`, appUrl);
-  const oneTimeUrl = url.href;
-  const txtFileName = 'userActivation';
-
-  return mailService.send({
-    to: email,
-    subject: '[GROWI] User Activation',
-    template: path.join(localeDir, `${i18n}/notifications/${txtFileName}.txt`),
-    vars: {
-      appTitle: appService.getAppTitle(),
-      email,
-      expiredAt: formattedExpiredAt,
-      url: oneTimeUrl,
-    },
-  });
-}
-
-export const registerAction = (crowi) => {
-  const User = crowi.model('User');
-
-  return async function(req, res) {
-    const registerForm = req.body.registerForm || {};
-    const email = registerForm.email;
-    const isRegisterableEmail = await User.isRegisterableEmail(email);
-
-    if (!isRegisterableEmail) {
-      req.body.registerForm.email = email;
-      req.flash('registerWarningMessage', req.t('message.email_address_is_already_registered'));
-      req.flash('email', email);
-
-      return res.redirect('/login#register');
-    }
-
-    makeRegistrationEmailToken(email, crowi);
-
-    req.flash('successMessage', req.t('message.successfully_send_email_auth', { email }));
-
-    return res.redirect('/login');
-  };
-};
-
 // middleware to handle error
 export const tokenErrorHandlerMiddeware = (err, req, res, next) => {
   if (err != null) {
@@ -75,47 +11,3 @@ export const tokenErrorHandlerMiddeware = (err, req, res, next) => {
   }
   next();
 };
-
-// validation rules for registration form when email authentication enabled
-export const registerRules = () => {
-  return [
-    body('registerForm.email')
-      .isEmail()
-      .withMessage('Email format is invalid.')
-      .exists()
-      .withMessage('Email field is required.'),
-  ];
-};
-
-// middleware to validate complete registration form
-export const validateCompleteRegistrationForm = (req, res, next) => {
-  const errors = validationResult(req);
-  if (errors.isEmpty()) {
-    return next();
-  }
-
-  const extractedErrors: string[] = [];
-  errors.array().map(err => extractedErrors.push(err.msg));
-
-  req.flash('errors', extractedErrors);
-  req.flash('inputs', req.body);
-
-  const token = req.body.token;
-  return res.redirect(`/user-activation/${token}`);
-};
-
-// middleware to validate register form if email authentication enabled
-export const validateRegisterForm = (req, res, next) => {
-  const errors = validationResult(req);
-  if (errors.isEmpty()) {
-    return next();
-  }
-
-  req.form = { isValid: false };
-  const extractedErrors: string[] = [];
-  errors.array().map(err => extractedErrors.push(err.msg));
-
-  req.flash('registerWarningMessage', extractedErrors);
-
-  res.redirect('back');
-};