Răsfoiți Sursa

Merge pull request #1648 from weseek/reactify/create-apiV3-update-password

Reactify/create api v3 update password
Yuki Takei 6 ani în urmă
părinte
comite
7224e0b318

+ 35 - 34
src/client/js/components/Me/PasswordSettings.jsx

@@ -25,33 +25,27 @@ class PasswordSettings extends React.Component {
       newPasswordConfirm: '',
     };
 
-    this.retrievePassword();
-
-    this.retrievePassword = this.retrievePassword.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
     this.onChangeOldPassword = this.onChangeOldPassword.bind(this);
-  }
 
-  async retrievePassword() {
-    try {
-    // TODO GW-1136 create apiV3 for updating password
-    }
-    catch (err) {
-      this.setState({ retrieveError: err });
-      toastError(err);
-    }
   }
 
   async onClickSubmit() {
-    const { t } = this.props;
+    const { t, appContainer, personalContainer } = this.props;
+    const { oldPassword, newPassword, newPasswordConfirm } = this.state;
 
     try {
-      // TODO GW-1136 create apiV3 for updating password
+      await appContainer.apiv3Put('/personal-setting/password', {
+        oldPassword, newPassword, newPasswordConfirm,
+      });
+      this.setState({ oldPassword: '', newPassword: '', newPasswordConfirm: '' });
+      await personalContainer.retrievePersonalData();
       toastSuccess(t('toaster.update_successed', { target: t('personal_settings.update_password') }));
     }
     catch (err) {
       toastError(err);
     }
+
   }
 
   onChangeOldPassword(oldPassword) {
@@ -66,32 +60,33 @@ class PasswordSettings extends React.Component {
     this.setState({ newPasswordConfirm });
   }
 
-
   render() {
-    const { t } = this.props;
+    const { t, personalContainer } = this.props;
+    const { newPassword, newPasswordConfirm } = this.state;
+    const isIncorrectConfirmPassword = (newPassword !== newPasswordConfirm);
 
     return (
       <React.Fragment>
-        {(this.state.password == null) && <div className="alert alert-warning m-t-10">{ t('Password is not set') }</div>}
+        {(!personalContainer.state.isPasswordSet) && <div className="alert alert-warning m-t-10">{ t('Password is not set') }</div>}
         <div className="mb-5 container-fluid">
-          {(this.state.password == null)
-            ? <h2 className="border-bottom">{t('personal_settings.set_new_password')}</h2>
-          : <h2 className="border-bottom">{t('personal_settings.update_password')}</h2>}
+          {(personalContainer.state.isPasswordSet)
+            ? <h2 className="border-bottom">{t('personal_settings.update_password')}</h2>
+          : <h2 className="border-bottom">{t('personal_settings.set_new_password')}</h2>}
         </div>
-        {(this.state.password != null)
+        {(personalContainer.state.isPasswordSet)
         && (
-        <div className="row mb-3">
-          <label htmlFor="oldPassword" className="col-xs-3 text-right">{ t('personal_settings.current_password') }</label>
-          <div className="col-xs-6">
-            <input
-              className="form-control"
-              type="password"
-              name="oldPassword"
-              value={this.state.oldPassword}
-              onChange={(e) => { this.onChangeOldPassword(e.target.value) }}
-            />
+          <div className="row mb-3">
+            <label htmlFor="oldPassword" className="col-xs-3 text-right">{ t('personal_settings.current_password') }</label>
+            <div className="col-xs-6">
+              <input
+                className="form-control"
+                type="password"
+                name="oldPassword"
+                value={this.state.oldPassword}
+                onChange={(e) => { this.onChangeOldPassword(e.target.value) }}
+              />
+            </div>
           </div>
-        </div>
         )}
         <div className="row mb-3">
           <label htmlFor="newPassword" className="col-xs-3 text-right">{t('personal_settings.new_password') }</label>
@@ -105,7 +100,7 @@ class PasswordSettings extends React.Component {
             />
           </div>
         </div>
-        <div className="row mb-3">
+        <div className={`row mb-3 ${isIncorrectConfirmPassword && 'has-error'}`}>
           <label htmlFor="newPasswordConfirm" className="col-xs-3 text-right">{t('personal_settings.new_password_confirm') }</label>
           <div className="col-xs-6">
             <input
@@ -122,7 +117,12 @@ class PasswordSettings extends React.Component {
 
         <div className="row my-3">
           <div className="col-xs-offset-4 col-xs-5">
-            <button type="button" className="btn btn-primary" onClick={this.onClickSubmit} disabled={this.state.retrieveError != null}>
+            <button
+              type="button"
+              className="btn btn-primary"
+              onClick={this.onClickSubmit}
+              disabled={this.state.retrieveError != null || isIncorrectConfirmPassword}
+            >
               {t('Update')}
             </button>
           </div>
@@ -141,6 +141,7 @@ const PasswordSettingsWrapper = (props) => {
 PasswordSettings.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  personalContainer: PropTypes.instanceOf(PersonalContainer).isRequired,
 };
 
 export default withTranslation()(PasswordSettingsWrapper);

+ 2 - 0
src/client/js/services/PersonalContainer.js

@@ -24,6 +24,7 @@ export default class PersonalContainer extends Container {
       isEmailPublished: false,
       lang: 'en-US',
       externalAccounts: [],
+      isPasswordSet: false,
     };
 
   }
@@ -48,6 +49,7 @@ export default class PersonalContainer extends Container {
         email: currentUser.email,
         isEmailPublished: currentUser.isEmailPublished,
         lang: currentUser.lang,
+        isPasswordSet: (currentUser.password != null),
       });
     }
     catch (err) {

+ 68 - 3
src/server/routes/apiv3/personal-setting.js

@@ -33,8 +33,19 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *            type: string
  *          isEmailPublished:
  *            type: boolean
+ *      Passwords:
+ *        description: passwords for update
+ *        type: object
+ *        properties:
+ *          oldPassword:
+ *            type: string
+ *          newPassword:
+ *            type: string
+ *          newPasswordConfirm:
+ *            type: string
  */
 module.exports = (crowi) => {
+  const accessTokenParser = require('../../middleware/access-token-parser')(crowi);
   const loginRequiredStrictly = require('../../middleware/login-required')(crowi);
   const csrf = require('../../middleware/csrf')(crowi);
 
@@ -50,6 +61,16 @@ module.exports = (crowi) => {
       body('lang').isString().isIn(['en-US', 'ja']),
       body('isEmailPublished').isBoolean(),
     ],
+    password: [
+      body('oldPassword').isString(),
+      body('newPassword').isString().not().isEmpty()
+        .isLength({ min: 6 })
+        .withMessage('password must be at least 6 characters long'),
+      body('newPasswordConfirm').isString().not().isEmpty()
+        .custom((value, { req }) => {
+          return (value === req.body.newPassword);
+        }),
+    ],
   };
 
   /**
@@ -72,7 +93,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: personal params
    */
-  router.get('/', loginRequiredStrictly, async(req, res) => {
+  router.get('/', accessTokenParser, loginRequiredStrictly, async(req, res) => {
     const currentUser = await User.findUserByUsername(req.user.username);
     return res.apiv3({ currentUser });
   });
@@ -103,7 +124,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: personal params
    */
-  router.put('/', loginRequiredStrictly, csrf, validator.personal, ApiV3FormValidator, async(req, res) => {
+  router.put('/', accessTokenParser, loginRequiredStrictly, csrf, validator.personal, ApiV3FormValidator, async(req, res) => {
 
     try {
       const user = await User.findOne({ _id: req.user.id });
@@ -143,7 +164,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: array of external accounts
    */
-  router.get('/external-accounts', loginRequiredStrictly, async(req, res) => {
+  router.get('/external-accounts', accessTokenParser, loginRequiredStrictly, async(req, res) => {
     const userData = req.user;
 
     try {
@@ -157,5 +178,49 @@ module.exports = (crowi) => {
 
   });
 
+  /**
+   * @swagger
+   *
+   *    /personal-setting/password:
+   *      put:
+   *        tags: [PersonalSetting]
+   *        operationId: putUserPassword
+   *        summary: /personal-setting/password
+   *        description: Update user password
+   *        requestBody:
+   *          required: true
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/Passwords'
+   *        responses:
+   *          200:
+   *            description: user password
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    userData:
+   *                      type: object
+   *                      description: user data updated
+   */
+  router.put('/password', accessTokenParser, loginRequiredStrictly, csrf, validator.password, ApiV3FormValidator, async(req, res) => {
+    const { body, user } = req;
+    const { oldPassword, newPassword } = body;
+
+    if (user.isPasswordSet() && !user.isPasswordValid(oldPassword)) {
+      return res.apiv3Err('wrong-current-password', 400);
+    }
+    try {
+      const userData = await user.updatePassword(newPassword);
+      return res.apiv3({ userData });
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err('update-password-failed');
+    }
+
+  });
+
   return router;
 };

+ 0 - 2
src/server/routes/index.js

@@ -128,7 +128,6 @@ module.exports = function(crowi, app) {
   app.get('/admin/export/:fileName'             , loginRequiredStrictly , adminRequired ,admin.export.download);
 
   app.get('/me'                       , loginRequiredStrictly , me.index);
-  app.get('/me/password'              , loginRequiredStrictly , me.password);
   app.get('/me/apiToken'              , loginRequiredStrictly , me.apiToken);
   app.post('/me'                      , loginRequiredStrictly , csrf , form.me.user , me.index);
   // external-accounts
@@ -136,7 +135,6 @@ module.exports = function(crowi, app) {
   app.post('/me/external-accounts/disassociate'           , loginRequiredStrictly , me.externalAccounts.disassociate);
   app.post('/me/external-accounts/associateLdap'          , loginRequiredStrictly , form.login , me.externalAccounts.associateLdap);
 
-  app.post('/me/password'             , form.me.password          , loginRequiredStrictly , me.password);
   app.post('/me/imagetype'            , form.me.imagetype         , loginRequiredStrictly , me.imagetype);
   app.post('/me/apiToken'             , form.me.apiToken          , loginRequiredStrictly , me.apiToken);
 

+ 0 - 53
src/server/routes/me.js

@@ -254,59 +254,6 @@ module.exports = function(crowi, app) {
     })(req, res, () => {});
   };
 
-  actions.password = function(req, res) {
-    const passwordForm = req.body.mePassword;
-    const userData = req.user;
-
-    /*
-      * disabled because the system no longer allows undefined email -- 2017.10.06 Yuki Takei
-      *
-    // パスワードを設定する前に、emailが設定されている必要がある (schemaを途中で変更したため、最初の方の人は登録されていないかもしれないため)
-    // そのうちこのコードはいらなくなるはず
-    if (!userData.isEmailSet()) {
-      return res.redirect('/me');
-    }
-    */
-
-    if (req.method === 'POST' && req.form.isValid) {
-      const newPassword = passwordForm.newPassword;
-      const newPasswordConfirm = passwordForm.newPasswordConfirm;
-      const oldPassword = passwordForm.oldPassword;
-
-      if (userData.isPasswordSet() && !userData.isPasswordValid(oldPassword)) {
-        req.form.errors.push('Wrong current password');
-        return res.render('me/password', {
-        });
-      }
-
-      // check password confirm
-      if (newPassword !== newPasswordConfirm) {
-        req.form.errors.push('Failed to verify passwords');
-      }
-      else {
-        userData.updatePassword(newPassword, (err, userData) => {
-          if (err) {
-            /* eslint-disable no-restricted-syntax, no-prototype-builtins */
-            for (const e in err.errors) {
-              if (err.errors.hasOwnProperty(e)) {
-                req.form.errors.push(err.errors[e].message);
-              }
-            }
-            return res.render('me/password', {});
-          }
-          /* eslint-enable no-restricted-syntax, no-prototype-builtins */
-
-          req.flash('successMessage', 'Password updated');
-          return res.redirect('/me/password');
-        });
-      }
-    }
-    else { // method GET
-      return res.render('me/password', {
-      });
-    }
-  };
-
   actions.apiToken = function(req, res) {
     const userData = req.user;
 

+ 0 - 100
src/server/views/me/password.html

@@ -1,100 +0,0 @@
-{% extends '../layout-growi/base/layout.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitle(t('Password Settings')) }}{% endblock %}
-
-{% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="mypage-title" class="title">{{ t('Password Settings') }}</h1>
-  </header>
-</div>
-{% endblock %}
-
-{% block content_main %}
-<div class="content-main">
-
-  <ul class="nav nav-tabs">
-    <li><a href="/me"><i class="icon-user"></i> {{ t('User Information') }}</a></li>
-    <li><a href="/me/external-accounts"><i class="icon-share-alt"></i> {{ t('External Accounts') }}</a></li>
-    <li class="active"><a href="/me/password"><i class="icon-lock"></i> {{ t('Password Settings') }}</a></li>
-    <li><a href="/me/apiToken"><i class="icon-paper-plane"></i> {{ t('API Settings') }}</a></li>
-  </ul>
-
-  <div class="tab-content">
-
-  {% if not user.password %}
-  <div class="alert alert-warning m-t-10">
-    {{ t('Password is not set') }}
-  </div>
-  {% endif %}
-
-  {% set message = req.flash('successMessage') %}
-  {% if message.length %}
-  <div class="alert alert-success m-t-10">
-    {{ message }}
-  </div>
-  {% endif %}
-
-  {% if req.form.errors.length > 0 %}
-  <div class="alert alert-danger m-t-10">
-    <ul>
-    {% for error in req.form.errors %}
-      <li>{{ error }}</li>
-    {% endfor %}
-    </ul>
-  </div>
-  {% endif %}
-
-  <div id="form-box" class="m-t-20">
-
-    <form action="/me/password" method="post" class="form-horizontal" role="form">
-    <fieldset>
-      {% if user.password %}
-      <legend>{{ t('Update Password') }}</legend>
-      {% else %}
-      <legend>{{ t('Set new Password') }}</legend>
-      {% endif %}
-      {% if user.password %}
-      <div class="form-group">
-        <label for="mePassword[oldPassword]" class="col-xs-3 control-label">{{ t('Current password') }}</label>
-        <div class="col-xs-6">
-          <input class="form-control" type="password" name="mePassword[oldPassword]">
-        </div>
-      </div>
-      {% endif %}
-      <div class="form-group {% if not user.password %}has-error{% endif %}">
-        <label for="mePassword[newPassword]" class="col-xs-3 control-label">{{ t('New password') }}</label>
-        <div class="col-xs-6">
-          <input class="form-control" type="password" name="mePassword[newPassword]" required>
-        </div>
-      </div>
-      <div class="form-group">
-        <label for="mePassword[newPasswordConfirm]" class="col-xs-3 control-label">{{ t('Re-enter new password') }}</label>
-        <div class="col-xs-6">
-          <input class="form-control col-xs-4" type="password" name="mePassword[newPasswordConfirm]" required>
-
-          <p class="help-block">{{ t('page_register.form_help.password') }}</p>
-        </div>
-      </div>
-
-
-      <div class="form-group">
-        <div class="text-center">
-          <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
-        </div>
-      </div>
-
-    </fieldset>
-    </form>
-  </div>
-
-
-  </div>
-</div>
-{% endblock content_main %}
-
-{% block content_footer %}
-{% endblock %}
-
-{% block layout_footer %}
-{% endblock %}