Browse Source

Merge pull request #152 from crowi/password-reset-only

Password reset
Sotaro KARASAWA 9 years ago
parent
commit
9074771b52
6 changed files with 220 additions and 2 deletions
  1. 78 2
      lib/models/user.js
  2. 31 0
      lib/routes/admin.js
  3. 3 0
      lib/routes/index.js
  4. 67 0
      lib/views/admin/users.html
  5. 29 0
      resource/js/crowi-admin.js
  6. 12 0
      test/models/user.test.js

+ 78 - 2
lib/models/user.js

@@ -11,9 +11,9 @@ module.exports = function(crowi) {
     , STATUS_SUSPENDED  = 3
     , STATUS_DELETED    = 4
     , STATUS_INVITED    = 5
-    , USER_PUBLIC_FIELDS = '_id image googleId name username email status createdAt' // TODO: どこか別の場所へ...
+    , USER_PUBLIC_FIELDS = '_id image googleId name username email introduction status createdAt admin' // TODO: どこか別の場所へ...
 
-    , PAGE_ITEMS        = 20
+    , PAGE_ITEMS        = 50
 
     , userEvent = crowi.event('user')
 
@@ -57,6 +57,19 @@ module.exports = function(crowi) {
     }
   }
 
+  function generateRandomTempPassword () {
+    var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!=-_';
+    var password = '';
+    var len = 12;
+
+    for (var i = 0; i < len; i++) {
+      var randomPoz = Math.floor(Math.random() * chars.length);
+      password += chars.substring(randomPoz, randomPoz+1);
+    }
+
+    return password;
+  }
+
   function generatePassword (password) {
     var hasher = crypto.createHash('sha256');
     hasher.update(crowi.env.PASSWORD_SEED + password);
@@ -244,6 +257,19 @@ module.exports = function(crowi) {
     return true;
   };
 
+  userSchema.statics.filterToPublicFields = function(user) {
+    var filteredUser = {};
+    var fields = USER_PUBLIC_FIELDS.split(' ');
+    for (var i = 0; i < fields.length; i++) {
+      var key = fields[i];
+      if (user[key]) {
+        filteredUser[key] = user[key];
+      }
+    }
+
+    return filteredUser;
+  };
+
   userSchema.statics.findUsers = function(options, callback) {
     var sort = options.sort || {status: 1, createdAt: 1};
 
@@ -326,6 +352,31 @@ module.exports = function(crowi) {
     }, { sortBy : sort });
   };
 
+  userSchema.statics.findUsersByPartOfEmail = function(emailPart, options) {
+    const status = options.status || null;
+    const emailPartRegExp = new RegExp(emailPart.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'));
+    const User = this;
+
+    return new Promise((resolve, reject) => {
+      const query = User.find({ email: emailPartRegExp }, USER_PUBLIC_FIELDS);
+
+      if (status) {
+        query.and({status});
+      }
+
+      query
+        .limit(PAGE_ITEMS + 1)
+        .exec((err, userData) => {
+          if (err) {
+            return reject(err);
+          }
+
+          return resolve(userData);
+        });
+    });
+  };
+
+
   userSchema.statics.findUserByUsername = function(username) {
     var User = this;
     return new Promise(function(resolve, reject) {
@@ -428,6 +479,30 @@ module.exports = function(crowi) {
     });
   };
 
+  userSchema.statics.resetPasswordByRandomString = function(id) {
+    var User = this;
+
+    return new Promise(function(resolve, reject) {
+      User.findById(id, function (err, userData) {
+        if (!userData) {
+          return reject(new Error('User not found'));
+        }
+
+        // is updatable check
+        // if (userData.isUp
+        var newPassword = generateRandomTempPassword();
+        userData.setPassword(newPassword);
+        userData.save(function(err, userData) {
+          if (err) {
+            return reject(err);
+          }
+
+          resolve({user: userData, newPassword: newPassword});
+        });
+      });
+    });
+  };
+
   userSchema.statics.createUsersByInvitation = function(emailList, toSendEmail, callback) {
     var User = this
       , createdUserList = []
@@ -573,6 +648,7 @@ module.exports = function(crowi) {
   userSchema.statics.STATUS_DELETED = STATUS_DELETED;
   userSchema.statics.STATUS_INVITED = STATUS_INVITED;
   userSchema.statics.USER_PUBLIC_FIELDS = USER_PUBLIC_FIELDS;
+  userSchema.statics.PAGE_ITEMS = PAGE_ITEMS;
 
   return mongoose.model('User', userSchema);
 };

+ 31 - 0
lib/routes/admin.js

@@ -337,6 +337,21 @@ module.exports = function(crowi, app) {
     });
   };
 
+  // app.post('/_api/admin/users.resetPassword' , admin.api.usersResetPassword);
+  actions.user.resetPassword = function(req, res) {
+    const id = req.body.user_id;
+    const User = crowi.model('User');
+
+    User.resetPasswordByRandomString(id)
+    .then(function(data) {
+      data.user = User.filterToPublicFields(data.user);
+      return res.json(ApiResponse.success(data));
+    }).catch(function(err) {
+      debug('Error on reseting password', err);
+      return res.json(ApiResponse.error('Error'));
+    });
+  }
+
   actions.api = {};
   actions.api.appSetting = function(req, res) {
     var form = req.form.settingForm;
@@ -399,6 +414,22 @@ module.exports = function(crowi, app) {
     });
   };
 
+  // app.get('/_api/admin/users.search' , admin.api.userSearch);
+  actions.api.usersSearch = function(req, res) {
+    const User = crowi.model('User');
+    const email =req.query.email;
+
+    User.findUsersByPartOfEmail(email, {})
+    .then(users => {
+      const result = {
+        data: users
+      };
+      return res.json(ApiResponse.success(result));
+    }).catch(err => {
+      return res.json(ApiResponse.error());
+    });
+  };
+
   function saveSetting(req, res, form)
   {
     Config.updateNamespaceByArray('crowi', form, function(err, config) {

+ 3 - 0
lib/routes/index.js

@@ -54,6 +54,7 @@ module.exports = function(crowi, app) {
   app.get('/admin/notification/slackAuth'    , loginRequired(crowi, app) , middleware.adminRequired() , admin.notification.slackAuth);
   app.post('/_api/admin/notification.add'    , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.api.notificationAdd);
   app.post('/_api/admin/notification.remove' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.api.notificationRemove);
+  app.get('/_api/admin/users.search'         , loginRequired(crowi, app) , middleware.adminRequired() , admin.api.usersSearch);
 
   app.get('/admin/users'                , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.index);
   app.post('/admin/user/invite'         , form.admin.userInvite ,  loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.invite);
@@ -63,6 +64,8 @@ module.exports = function(crowi, app) {
   app.post('/admin/user/:id/suspend'    , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.suspend);
   app.post('/admin/user/:id/remove'     , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.remove);
   app.post('/admin/user/:id/removeCompletely' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.removeCompletely);
+  // new route patterns from here:
+  app.post('/_api/admin/users.resetPassword'  , loginRequired(crowi, app) , middleware.adminRequired() , csrf, admin.user.resetPassword);
 
   app.get('/me'                       , loginRequired(crowi, app) , me.index);
   app.get('/me/password'              , loginRequired(crowi, app) , me.password);

+ 67 - 0
lib/views/admin/users.html

@@ -76,6 +76,62 @@
       </div><!-- /.modal -->
       {% endif %}
 
+      {# FIXME とりあえずクソ実装。React化はやくしたいなー(チラッチラッ #}
+      <div class="modal fade" id="admin-password-reset-modal">
+        <div class="modal-dialog">
+          <div class="modal-content">
+            <div class="modal-header">
+              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+              <h4 class="modal-title">パスワードを新規発行しますか?</h4>
+            </div>
+
+            <div class="modal-body">
+              <p>
+              新規発行したパスワードはこの画面を閉じると二度と表示できませんのでご注意ください。<br>
+              <span class="text-danger">新規発行したパスワードを、対象ユーザーへ連絡してください。</span>
+              </p>
+              <p>
+              Reset user: <code id="admin-password-reset-user"></code>
+              </p>
+
+              <form method="post" id="admin-users-reset-password">
+                <input type="hidden" name="user_id" value="">
+                <input type="hidden" name="_csrf" value="{{ csrf() }}">
+                <button type="submit" value="" class="btn btn-primary">
+                  実行
+                </button>
+              </form>
+
+            </div>
+
+          </div><!-- /.modal-content -->
+        </div><!-- /.modal-dialog -->
+      </div>
+      <div class="modal fade" id="admin-password-reset-modal-done">
+        <div class="modal-dialog">
+          <div class="modal-content">
+
+            <div class="modal-header">
+              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+              <h4 class="modal-title">Password reset!</h4>
+            </div>
+
+            <div class="modal-body">
+              <p class="alert alert-danger">Let the user know the new password below and strongly recommend to change another one immediately. </p>
+              <p>
+              Reset user: <code id="admin-password-reset-done-user"></code>
+              </p>
+              <p>
+              New passwrod: <code id="admin-password-reset-done-password"></code>
+              </p>
+            </div>
+            <div class="modal-footer">
+              <button class="btn btn-primary" data-dismiss="modal">OK</button>
+            </div>
+          </div><!-- /.modal-content -->
+        </div><!-- /.modal-dialog -->
+      </div>
+
       <h2>ユーザー一覧</h2>
 
       <table class="table table-hover table-striped table-bordered">
@@ -121,6 +177,16 @@
                   <li class="dropdown-header">編集メニュー</li>
                   <li>
                     <a href="">編集</a>
+
+                  </li>
+                  <li class="dropdown-button">
+                    <a href="#"
+                      data-user-id="{{ sUser._id.toString() }}"
+                      data-user-email="{{ sUser.email }}"
+                      data-target="#admin-password-reset-modal"
+                      data-toggle="modal" class="btn btn-block btn-default">
+                      パスワードの再発行
+                    </a>
                   </li>
                   <li class="divider"></li>
                   <li class="dropdown-header">ステータス</li>
@@ -132,6 +198,7 @@
                   </form>
                   {% endif  %}
                   {% if sUser.status == 2 %}
+
                   <form action="/admin/user/{{ sUser._id.toString() }}/suspend" method="post">
                     <input type="hidden" name="_csrf" value="{{ csrf() }}">
                     <button type="submit" class="btn btn-block btn-warning">アカウント停止</button>

+ 29 - 0
resource/js/crowi-admin.js

@@ -23,4 +23,33 @@ $(function() {
   });
 
   $('#createdUserModal').modal('show');
+
+  $('#admin-password-reset-modal').on('show.bs.modal', function(button) {
+    var data = $(button.relatedTarget);
+    var userId = data.data('user-id');
+    var email = data.data('user-email');
+
+    $('#admin-password-reset-user').text(email);
+    $('#admin-users-reset-password input[name=user_id]').val(userId);
+  });
+
+  $('form#admin-users-reset-password').on('submit', function(e) {
+    $.post('/_api/admin/users.resetPassword', $(this).serialize(), function(res) {
+      if (res.ok) {
+        // TODO Fix
+        //location.reload();
+        $('#admin-password-reset-modal').modal('hide');
+        $('#admin-password-reset-modal-done').modal('show');
+
+        $("#admin-password-reset-done-user").text(res.user.email);
+        $("#admin-password-reset-done-password").text(res.newPassword);
+        return ;
+      }
+
+      // fixme
+      alert('Failed to reset password');
+    });
+
+    return false;
+  });
 });

+ 12 - 0
test/models/user.test.js

@@ -29,6 +29,18 @@ describe('User', function () {
           done();
         });
       });
+
+      it('should be found by findUsersByPartOfEmail', function(done) {
+
+        User.findUsersByPartOfEmail('ao', {})
+        .then(function(userData) {
+          expect(userData).to.be.array;
+          expect(userData[0]).to.instanceof(User);
+          expect(userData[0].email).to.equal('aoi@example.com');
+          done();
+        });
+
+      });
     });
   });