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

Merge pull request #6700 from weseek/feat/convert-form-to-xhr-for-invitedForm-integrate

feat: Convert form to xhr for invited form integrate
ryoji-s 3 лет назад
Родитель
Сommit
fecbf95089

+ 1 - 1
packages/app/config/rate-limiter.ts

@@ -33,7 +33,7 @@ export const defaultConfig: IApiRateLimitEndpointMap = {
     maxRequests: MAX_REQUESTS_TIER_1,
     usersPerIpProspection: 100,
   },
-  '/invited/activateInvited': {
+  '/invited': {
     method: 'POST',
     maxRequests: MAX_REQUESTS_TIER_2,
   },

+ 72 - 12
packages/app/src/components/InvitedForm.tsx

@@ -1,8 +1,12 @@
-import React from 'react';
+import React, { useCallback, useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
+
+import { apiv3Post } from '~/client/util/apiv3-client';
+
+import { useCurrentUser } from '../stores/context';
 
-import { useCsrfToken, useCurrentUser } from '../stores/context';
 
 export type InvitedFormProps = {
   invitedFormUsername: string,
@@ -10,23 +14,79 @@ export type InvitedFormProps = {
 }
 
 export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
+
   const { t } = useTranslation();
-  const { data: csrfToken } = useCsrfToken();
+  const router = useRouter();
   const { data: user } = useCurrentUser();
+  const [isConnectSuccess, setIsConnectSuccess] = useState<boolean>(false);
+  const [loginErrors, setLoginErrors] = useState<Error[]>([]);
 
   const { invitedFormUsername, invitedFormName } = props;
 
+  const submitHandler = useCallback(async(e) => {
+    e.preventDefault();
+
+    const formData = e.target.elements;
+
+    const {
+      'invitedForm[name]': { value: name },
+      'invitedForm[password]': { value: password },
+      'invitedForm[username]': { value: username },
+    } = formData;
+
+    const invitedForm = {
+      name,
+      password,
+      username,
+    };
+
+    try {
+      const res = await apiv3Post('/invited', { invitedForm });
+      setIsConnectSuccess(true);
+      const { redirectTo } = res.data;
+      router.push(redirectTo);
+    }
+    catch (err) {
+      setLoginErrors(err);
+    }
+  }, [router]);
+
+  const formNotification = useCallback(() => {
+
+    if (isConnectSuccess) {
+      return (
+        <p className="alert alert-success">
+          <strong>{ t('message.successfully_connected') }</strong><br></br>
+        </p>
+      );
+    }
+
+    return (
+      <>
+        { loginErrors != null && loginErrors.length > 0 ? (
+          <p className="alert alert-danger">
+            { loginErrors.map((err, index) => {
+              return <span key={index}>{ t(err.message) }<br/></span>;
+            }) }
+          </p>
+        ) : (
+          <p className="alert alert-success">
+            <strong>{ t('invited.discription_heading') }</strong><br></br>
+            <small>{ t('invited.discription') }</small>
+          </p>
+        ) }
+      </>
+    );
+  }, [isConnectSuccess, loginErrors, t]);
+
   if (user == null) {
     return <></>;
   }
 
   return (
-    <div className="noLogin-dialog p-3 mx-auto" id="noLogin-dialog">
-      <p className="alert alert-success">
-        <strong>{ t('invited.discription_heading') }</strong><br></br>
-        <small>{ t('invited.discription') }</small>
-      </p>
-      <form role="form" action="/invited/activateInvited" method="post" id="invited-form">
+    <div className="noLogin-dialog px-3 pb-3 mx-auto" id="noLogin-dialog">
+      { formNotification() }
+      <form role="form" onSubmit={submitHandler} id="invited-form">
         {/* Email Form */}
         <div className="input-group">
           <div className="input-group-prepend">
@@ -89,11 +149,11 @@ export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
             placeholder={t('Password')}
             name="invitedForm[password]"
             required
+            minLength={6}
           />
         </div>
         {/* Create Button */}
-        <div className="input-group justify-content-center d-flex mt-5">
-          <input type="hidden" name="_csrf" value={csrfToken} />
+        <div className="input-group justify-content-center d-flex mt-4">
           <button type="submit" className="btn btn-fill" id="register">
             <div className="eff"></div>
             <span className="btn-label"><i className="icon-user-follow"></i></span>
@@ -101,7 +161,7 @@ export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
           </button>
         </div>
       </form>
-      <div className="input-group mt-5 d-flex justify-content-center">
+      <div className="input-group mt-4 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>

+ 45 - 0
packages/app/src/server/middlewares/invited-form-validator.ts

@@ -0,0 +1,45 @@
+import { NextFunction, Response } from 'express';
+import { body, validationResult, ValidationChain } from 'express-validator';
+import { Request } from 'express-validator/src/base';
+
+const MININUM_PASSWORD_LENGTH = 6;
+
+export const invitedRules = (): ValidationChain[] => {
+  return [
+    body('invitedForm.username')
+      .matches(/^[\da-zA-Z\-_.]+$/)
+      .withMessage('message.Username has invalid characters')
+      .not()
+      .isEmpty()
+      .withMessage('message.Username field is required'),
+    body('invitedForm.name')
+      .not()
+      .isEmpty()
+      .withMessage('message.Name field is required'),
+    body('invitedForm.password')
+      .matches(/^[\x20-\x7F]*$/)
+      .withMessage('message.Password has invalid character')
+      .isLength({ min: MININUM_PASSWORD_LENGTH })
+      .withMessage(`message.Password minimum character should be more than ${MININUM_PASSWORD_LENGTH} characters`)
+      .not()
+      .isEmpty()
+      .withMessage('message.Password field is required'),
+  ];
+};
+
+export const invitedValidation = (req: Request, _res: Response, next: () => NextFunction): any => {
+  const form = req.body;
+  const errors = validationResult(req);
+  const extractedErrors: string[] = [];
+
+  if (errors.isEmpty()) {
+    Object.assign(form, { isValid: true });
+  }
+  else {
+    errors.array().map(err => extractedErrors.push(err.msg));
+    Object.assign(form, { isValid: false, errors: extractedErrors });
+  }
+
+  req.form = form;
+  return next();
+};

+ 0 - 43
packages/app/src/server/middlewares/login-form-validator.ts

@@ -1,48 +1,5 @@
 import { body, validationResult } from 'express-validator';
 
-// form rules
-export const inviteRules = () => {
-  return [
-    body('invitedForm.username')
-      .matches(/^[\da-zA-Z\-_.]+$/)
-      .withMessage('Username has invalid characters')
-      .not()
-      .isEmpty()
-      .withMessage('Username field is required'),
-    body('invitedForm.name').not().isEmpty().withMessage('Name field is required'),
-    body('invitedForm.password')
-      .matches(/^[\x20-\x7F]*$/)
-      .withMessage('Password has invalid character')
-      .isLength({ min: 6 })
-      .withMessage('Password minimum character should be more than 6 characters')
-      .not()
-      .isEmpty()
-      .withMessage('Password field is required'),
-  ];
-};
-
-// validation action
-export const inviteValidation = (req, res, next) => {
-  const form = req.body;
-
-  const errors = validationResult(req);
-  if (errors.isEmpty()) {
-    Object.assign(form, { isValid: true });
-    req.form = form;
-    return next();
-  }
-
-  const extractedErrors: string[] = [];
-  errors.array().map(err => extractedErrors.push(err.msg));
-
-  req.flash('errorMessages', extractedErrors);
-
-  Object.assign(form, { isValid: false });
-  req.form = form;
-
-  return next();
-};
-
 // form rules
 export const loginRules = () => {
   return [

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

@@ -49,11 +49,13 @@ module.exports = (crowi, app, isInstalled) => {
   routerForAuth.post('/login', applicationInstalled, loginFormValidator.loginRules(), loginFormValidator.loginValidation,
     addActivity, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.cannotLoginErrorHadnler, loginPassport.loginFailure);
 
+  routerForAuth.use('/invited', require('./invited')(crowi));
   routerForAuth.use('/logout', require('./logout')(crowi));
 
   routerForAuth.post('/register',
     applicationInstalled, registerFormValidator.registerRules(), registerFormValidator.registerValidation, addActivity, login.register);
 
+
   // installer
   if (!isInstalled) {
     routerForAdmin.use('/installer', require('./installer')(crowi));

+ 53 - 0
packages/app/src/server/routes/apiv3/invited.ts

@@ -0,0 +1,53 @@
+import express, { Request, Router } from 'express';
+
+import Crowi from '../../crowi';
+import { invitedRules, invitedValidation } from '../../middlewares/invited-form-validator';
+
+import { ApiV3Response } from './interfaces/apiv3-response';
+
+type InvitedFormRequest = Request & { form: any, user: any };
+
+module.exports = (crowi: Crowi): Router => {
+  const applicationInstalled = require('../../middlewares/application-installed')(crowi);
+  const debug = require('debug')('growi:routes:login');
+  const User = crowi.model('User');
+  const router = express.Router();
+
+  router.post('/', applicationInstalled, invitedRules(), invitedValidation, async(req: InvitedFormRequest, res: ApiV3Response) => {
+    if (!req.user) {
+      return res.apiv3({ redirectTo: '/login' });
+    }
+
+    if (!req.form.isValid) {
+      return res.apiv3Err(req.form.errors, 400);
+    }
+
+    const user = req.user;
+    const invitedForm = req.form.invitedForm || {};
+    const username = invitedForm.username;
+    const name = invitedForm.name;
+    const password = invitedForm.password;
+
+    // check user upper limit
+    const isUserCountExceedsUpperLimit = await User.isUserCountExceedsUpperLimit();
+    if (isUserCountExceedsUpperLimit) {
+      return res.apiv3Err('message.can_not_activate_maximum_number_of_users', 403);
+    }
+
+    const creatable = await User.isRegisterableUsername(username);
+    if (!creatable) {
+      debug('username', username);
+      return res.apiv3Err('message.unable_to_use_this_user', 403);
+    }
+
+    try {
+      await user.activateInvitedUser(username, name, password);
+      return res.apiv3({ redirectTo: '/' });
+    }
+    catch (err) {
+      return res.apiv3Err('message.failed_to_activate', 403);
+    }
+  });
+
+  return router;
+};

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

@@ -81,7 +81,7 @@ module.exports = function(crowi, app) {
   app.get('/login/error/:reason'      , applicationInstalled, login.error);
   app.get('/login'                    , applicationInstalled, login.preLogin, next.delegateToNext);
   app.get('/invited'                  , applicationInstalled, next.delegateToNext);
-  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.get('/register'                 , applicationInstalled, login.preLogin, login.register);
 

+ 6 - 1
packages/app/src/server/routes/login-passport.js

@@ -11,6 +11,7 @@ module.exports = function(crowi, app) {
   const logger = loggerFactory('growi:routes:login-passport');
   const passport = require('passport');
   const ExternalAccount = crowi.model('ExternalAccount');
+  const User = crowi.model('User');
   const passportService = crowi.passportService;
 
   const activityEvent = crowi.event('activity');
@@ -91,6 +92,7 @@ module.exports = function(crowi, app) {
    * @param {*} res
    */
   const loginSuccessHandler = async(req, res, user, action) => {
+
     // update lastLoginAt
     user.updateLastLoginAt(new Date(), (err, userData) => {
       if (err) {
@@ -99,7 +101,9 @@ module.exports = function(crowi, app) {
       }
     });
 
-    const { redirectTo } = req.session;
+    // check for redirection to '/invited'
+    const redirectTo = req.user.status === User.STATUS_INVITED ? '/invited' : req.session.redirectTo;
+
     // remove session.redirectTo
     delete req.session.redirectTo;
 
@@ -112,6 +116,7 @@ module.exports = function(crowi, app) {
         username: req.user.username,
       },
     };
+
     await crowi.activityService.createActivity(parameters);
 
     return res.apiv3({ redirectTo });

+ 0 - 45
packages/app/src/server/routes/login.js

@@ -169,50 +169,5 @@ module.exports = function(crowi, app) {
     });
   };
 
-  actions.invited = async function(req, res) {
-    if (!req.user) {
-      return res.redirect('/login');
-    }
-
-    if (req.method === 'POST' && req.form.isValid) {
-      const user = req.user;
-      const invitedForm = req.form.invitedForm || {};
-      const username = invitedForm.username;
-      const name = invitedForm.name;
-      const password = invitedForm.password;
-
-      // check user upper limit
-      const isUserCountExceedsUpperLimit = await User.isUserCountExceedsUpperLimit();
-      if (isUserCountExceedsUpperLimit) {
-        req.flash('warningMessage', req.t('message.can_not_activate_maximum_number_of_users'));
-        return res.redirect('/invited');
-      }
-
-      const creatable = await User.isRegisterableUsername(username);
-      if (creatable) {
-        try {
-          await user.activateInvitedUser(username, name, password);
-          return res.redirect('/');
-        }
-        catch (err) {
-          req.flash('warningMessage', req.t('message.failed_to_activate'));
-          return res.render('invited');
-        }
-      }
-      else {
-        req.flash('warningMessage', req.t('message.unable_to_use_this_user'));
-        debug('username', username);
-        return res.render('invited');
-      }
-    }
-    else {
-      return res.render('invited');
-    }
-  };
-
-  actions.updateInvitedUser = function(req, res) {
-    return res.redirect('/');
-  };
-
   return actions;
 };