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

Merge pull request #6678 from weseek/feat/convert-form-to-xhr

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

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

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

+ 36 - 5
packages/app/src/components/InvitedForm.tsx

@@ -1,8 +1,12 @@
-import React from 'react';
+import React, { useCallback } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 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 = {
 export type InvitedFormProps = {
   invitedFormUsername: string,
   invitedFormUsername: string,
@@ -10,12 +14,40 @@ export type InvitedFormProps = {
 }
 }
 
 
 export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
 export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
+
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { data: csrfToken } = useCsrfToken();
+  const router = useRouter();
   const { data: user } = useCurrentUser();
   const { data: user } = useCurrentUser();
 
 
   const { invitedFormUsername, invitedFormName } = props;
   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 });
+      const { redirectTo } = res.data;
+      router.push(redirectTo);
+    }
+    catch (err) {
+      // TODO: show errors
+    }
+  }, [router]);
+
   if (user == null) {
   if (user == null) {
     return <></>;
     return <></>;
   }
   }
@@ -26,7 +58,7 @@ export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
         <strong>{ t('invited.discription_heading') }</strong><br></br>
         <strong>{ t('invited.discription_heading') }</strong><br></br>
         <small>{ t('invited.discription') }</small>
         <small>{ t('invited.discription') }</small>
       </p>
       </p>
-      <form role="form" action="/invited/activateInvited" method="post" id="invited-form">
+      <form role="form" onSubmit={submitHandler} id="invited-form">
         {/* Email Form */}
         {/* Email Form */}
         <div className="input-group">
         <div className="input-group">
           <div className="input-group-prepend">
           <div className="input-group-prepend">
@@ -93,7 +125,6 @@ export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
         </div>
         </div>
         {/* Create Button */}
         {/* Create Button */}
         <div className="input-group justify-content-center d-flex mt-5">
         <div className="input-group justify-content-center d-flex mt-5">
-          <input type="hidden" name="_csrf" value={csrfToken} />
           <button type="submit" className="btn btn-fill" id="register">
           <button type="submit" className="btn btn-fill" id="register">
             <div className="eff"></div>
             <div className="eff"></div>
             <span className="btn-label"><i className="icon-user-follow"></i></span>
             <span className="btn-label"><i className="icon-user-follow"></i></span>

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

@@ -0,0 +1,48 @@
+import { body, validationResult } from 'express-validator';
+
+// form rules
+export const invitedRules = () => {
+  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: 6 })
+      .withMessage('message.Password minimum character should be more than 6 characters')
+      .not()
+      .isEmpty()
+      .withMessage('message.Password field is required'),
+  ];
+};
+
+// validation action
+export const invitedValidation = (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));
+
+  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';
 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
 // form rules
 export const loginRules = () => {
 export const loginRules = () => {
   return [
   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,
   routerForAuth.post('/login', applicationInstalled, loginFormValidator.loginRules(), loginFormValidator.loginValidation,
     addActivity, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.cannotLoginErrorHadnler, loginPassport.loginFailure);
     addActivity, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.cannotLoginErrorHadnler, loginPassport.loginFailure);
 
 
+  routerForAuth.use('/invited', require('./invited')(crowi));
   routerForAuth.use('/logout', require('./logout')(crowi));
   routerForAuth.use('/logout', require('./logout')(crowi));
 
 
   routerForAuth.post('/register',
   routerForAuth.post('/register',
     applicationInstalled, registerFormValidator.registerRules(), registerFormValidator.registerValidation, addActivity, login.register);
     applicationInstalled, registerFormValidator.registerRules(), registerFormValidator.registerValidation, addActivity, login.register);
 
 
+
   // installer
   // installer
   if (!isInstalled) {
   if (!isInstalled) {
     routerForAdmin.use('/installer', require('./installer')(crowi));
     routerForAdmin.use('/installer', require('./installer')(crowi));

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

@@ -0,0 +1,58 @@
+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.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.apiv3({ redirectTo: '/invited' });
+      }
+
+      const creatable = await User.isRegisterableUsername(username);
+      if (creatable) {
+        try {
+          await user.activateInvitedUser(username, name, password);
+          return res.apiv3({ redirectTo: '/' });
+        }
+        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');
+    }
+  });
+
+  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/error/:reason'      , applicationInstalled, login.error);
   app.get('/login'                    , applicationInstalled, login.preLogin, next.delegateToNext);
   app.get('/login'                    , applicationInstalled, login.preLogin, next.delegateToNext);
   app.get('/invited'                  , applicationInstalled, 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);
   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 logger = loggerFactory('growi:routes:login-passport');
   const passport = require('passport');
   const passport = require('passport');
   const ExternalAccount = crowi.model('ExternalAccount');
   const ExternalAccount = crowi.model('ExternalAccount');
+  const User = crowi.model('User');
   const passportService = crowi.passportService;
   const passportService = crowi.passportService;
 
 
   const activityEvent = crowi.event('activity');
   const activityEvent = crowi.event('activity');
@@ -91,6 +92,7 @@ module.exports = function(crowi, app) {
    * @param {*} res
    * @param {*} res
    */
    */
   const loginSuccessHandler = async(req, res, user, action) => {
   const loginSuccessHandler = async(req, res, user, action) => {
+
     // update lastLoginAt
     // update lastLoginAt
     user.updateLastLoginAt(new Date(), (err, userData) => {
     user.updateLastLoginAt(new Date(), (err, userData) => {
       if (err) {
       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;
+
     // remove session.redirectTo
     // remove session.redirectTo
     delete req.session.redirectTo;
     delete req.session.redirectTo;
 
 
@@ -112,6 +116,7 @@ module.exports = function(crowi, app) {
         username: req.user.username,
         username: req.user.username,
       },
       },
     };
     };
+
     await crowi.activityService.createActivity(parameters);
     await crowi.activityService.createActivity(parameters);
 
 
     return res.apiv3({ redirectTo });
     return res.apiv3({ redirectTo });

+ 12 - 57
packages/app/src/server/routes/login.js

@@ -90,18 +90,18 @@ module.exports = function(crowi, app) {
 
 
   actions.preLogin = function(req, res, next) {
   actions.preLogin = function(req, res, next) {
     // user has already logged in
     // user has already logged in
-    // const { user } = req;
-    // if (user != null && user.status === User.STATUS_ACTIVE) {
-    //   const { redirectTo } = req.session;
-    //   // remove session.redirectTo
-    //   delete req.session.redirectTo;
-    //   return res.safeRedirect(redirectTo);
-    // }
-
-    // // set referer to 'redirectTo'
-    // if (req.session.redirectTo == null && req.headers.referer != null) {
-    //   req.session.redirectTo = req.headers.referer;
-    // }
+    const { user } = req;
+    if (user != null && user.status === User.STATUS_ACTIVE) {
+      const { redirectTo } = req.session;
+      // remove session.redirectTo
+      delete req.session.redirectTo;
+      return res.safeRedirect(redirectTo);
+    }
+
+    // set referer to 'redirectTo'
+    if (req.session.redirectTo == null && req.headers.referer != null) {
+      req.session.redirectTo = req.headers.referer;
+    }
 
 
     next();
     next();
   };
   };
@@ -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;
   return actions;
 };
 };