ソースを参照

Merge branch 'feat/107958/enable-email-sending-for-email-authentication' into feat/107627/user-activation-page

Shun Miyazawa 3 年 前
コミット
12026d3767

+ 24 - 4
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 [emailForActivationUser, setEmailForActivationUser] = useState('');
+  const [isSuccessToSendUserActivationEmail, setIsSuccessToSendUserActivationEmail] = useState(false);
+
 
   useEffect(() => {
     const { hash } = window.location;
@@ -261,6 +265,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
   const handleRegisterFormSubmit = useCallback(async(e, requestPath) => {
     e.preventDefault();
+    setIsSuccessToSendUserActivationEmail(false);
 
     const registerForm = {
       username: usernameForRegister,
@@ -272,6 +277,11 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       const res = await apiv3Post(requestPath, { registerForm });
       const { redirectTo } = res.data;
       router.push(redirectTo ?? '/');
+
+      if (isEmailAuthenticationEnabled) {
+        setEmailForActivationUser(emailForRegister);
+        setIsSuccessToSendUserActivationEmail(true);
+      }
     }
     catch (err) {
       // Execute if error exists
@@ -280,7 +290,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;
@@ -331,6 +341,14 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
           )
         }
 
+        {
+          (isEmailAuthenticationEnabled && isSuccessToSendUserActivationEmail) && (
+            <p className="alert alert-success">
+              <span>{t('message.successfully_send_email_auth', { email: emailForActivationUser })}</span>
+            </p>
+          )
+        }
+
         <form role="form" onSubmit={e => handleRegisterFormSubmit(e, registerAction) } id="register-form">
 
           {!isEmailAuthenticationEnabled && (
@@ -452,9 +470,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,
+    isSuccessToSendUserActivationEmail, props.email, props.name, props.username,
+    registerErrors, registrationMode, registrationWhiteList, emailForActivationUser, switchForm, t,
+  ]);
 
   return (
     <div className="noLogin-dialog mx-auto" id="noLogin-dialog">

+ 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) {

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

@@ -1,9 +1,11 @@
 import path from 'path';
 
 import { ErrorV3 } from '@growi/core';
-import * as express from 'express';
+import { format, subSeconds } from 'date-fns';
 import { body, validationResult } from 'express-validator';
 
+import UserRegistrationOrder from '~/server/models/user-registration-order';
+
 const PASSOWRD_MINIMUM_NUMBER = 8;
 // validation rules for complete registration form
 export const completeRegistrationRules = () => {
@@ -138,3 +140,79 @@ 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 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);
+    }
+
+    makeRegistrationEmailToken(email, crowi);
+
+    return res.apiv3({ redirectTo: '/login#register' });
+  };
+};

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

@@ -240,7 +240,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');
-};