Sotaro KARASAWA 11 лет назад
Родитель
Сommit
20ff3bac78
7 измененных файлов с 214 добавлено и 6 удалено
  1. 9 0
      form/admin/userInvite.js
  2. 7 3
      lib/middlewares.js
  3. 2 0
      lib/swigFunctions.js
  4. 97 0
      models/user.js
  5. 38 0
      routes/admin.js
  6. 3 0
      routes/index.js
  7. 58 3
      views/admin/users.html

+ 9 - 0
form/admin/userInvite.js

@@ -0,0 +1,9 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field;
+
+module.exports = form(
+  field('inviteForm[emailList]', '招待メールアドレス').trim().required(),
+  field('inviteForm[sendEmail]').trim()
+);

+ 7 - 3
lib/middlewares.js

@@ -84,14 +84,18 @@ exports.loginRequired = function(app) {
     var models = app.set('models');
 
     if (req.user && '_id' in req.user) {
-      if (req.user.status === models.User.STATUS_REGISTERED) {
+      if (req.user.status === models.User.STATUS_ACTIVE) {
+        // Active の人だけ先に進める
+        return next();
+      } else if (req.user.status === models.User.STATUS_REGISTERED) {
         return res.redirect('/login/error/registered');
       } else if (req.user.status === models.User.STATUS_SUSPENDED) {
         return res.redirect('/login/error/suspended');
+      } else if (req.user.status === models.User.STATUS_INVITED) {
+        return res.redirect('/login/invited');
       }
-
-      return next();
     }
+
     req.session.jumpTo = req.originalUrl;
     return res.redirect('/login');
   };

+ 2 - 0
lib/swigFunctions.js

@@ -52,6 +52,8 @@ module.exports = function(app) {
             return 'label-warning';
           case User.STATUS_DELETED:
             return 'label-danger';
+          case User.STATUS_INVITED:
+            return 'label-info';
           default:
             break;
         }

+ 97 - 0
models/user.js

@@ -3,6 +3,7 @@ module.exports = function(app, models) {
     , mongoosePaginate = require('mongoose-paginate')
     , debug = require('debug')('crowi:models:user')
     , crypto = require('crypto')
+    , async = require('async')
     , config = app.set('config')
     , ObjectId = mongoose.Schema.Types.ObjectId
 
@@ -289,6 +290,102 @@ module.exports = function(app, models) {
     });
   };
 
+  userSchema.statics.removeCompletelyById = function(id, callback) {
+    var User = this;
+    User.findById(id, function (err, userData) {
+      if (!userData) {
+        return callback(err, null);
+      }
+
+      debug('Removing user:', userData);
+      // 物理削除可能なのは、招待中ユーザーのみ
+      // 利用を一度開始したユーザーは論理削除のみ可能
+      if (userData.status !== STATUS_INVITED) {
+        return callback(new Error('Cannot remove completely the user whoes status is not INVITED'), null);
+      }
+
+      userData.remove(function(err) {
+        if (err) {
+          return callback(err, null);
+        }
+
+        return callback(null, 1);
+      });
+    });
+  };
+
+  userSchema.statics.createUsersByInvitation = function(emailList, toSendEmail, callback) {
+    var User = this
+      , createdUserList = [];
+
+    if (!Array.isArray(emailList)) {
+      debug('emailList is not array');
+    }
+
+    async.each(
+      emailList,
+      function(email, next) {
+        var newUser = new User()
+          ,password;
+
+        email = email.trim();
+
+        // email check
+        // TODO: 削除済みはチェック対象から外そう〜
+        User.findOne({email: email}, function (err, userData) {
+          if (userData) {
+            createdUserList.push({
+              email: email,
+              password: null,
+              user: null,
+            });
+
+            return next();
+          }
+
+          password = Math.random().toString(36).slice(-16);
+
+          newUser.email = email;
+          newUser.setPassword(password);
+          newUser.createdAt = Date.now();
+          newUser.status = STATUS_INVITED;
+
+          newUser.save(function(err, userData) {
+            if (err) {
+              createdUserList.push({
+                email: email,
+                password: null,
+                user: null,
+              });
+              debug('save failed!! ', email);
+            } else {
+              createdUserList.push({
+                email: email,
+                password: password,
+                user: userData,
+              });
+              debug('saved!', email);
+            }
+
+            next();
+          });
+        });
+      },
+      function(err) {
+        if (err) {
+          debug('error occured while iterate email list');
+        }
+
+        if (toSendEmail) {
+          debug('send Email here');
+        }
+
+        debug("createdUserList!!! ", createdUserList);
+        return callback(null, createdUserList);
+      }
+    );
+  };
+
   userSchema.statics.createUserByEmailAndPassword = function(name, username, email, password, callback) {
     var User = this
       , newUser = new User();

+ 38 - 0
routes/admin.js

@@ -94,6 +94,24 @@ module.exports = function(app) {
     });
   };
 
+  actions.user.invite = function(req, res) {
+    var form = req.form.inviteForm;
+    var toSendEmail = form.sendEmail || false;
+    if (req.form.isValid) {
+      User.createUsersByInvitation(form.emailList.split('\n'), toSendEmail, function(err, userList) {
+        if (err) {
+          req.flash('errorMessage', req.form.errors.join('\n'));
+        } else {
+          req.flash('createdUser', userList);
+        }
+        return res.redirect('/admin/users');
+      });
+    } else {
+      req.flash('errorMessage', req.form.errors.join('\n'));
+      return res.redirect('/admin/users');
+    }
+  };
+
   actions.user.makeAdmin = function(req, res) {
     var id = req.params.id;
     User.findById(id, function(err, userData) {
@@ -155,6 +173,26 @@ module.exports = function(app) {
     });
   };
 
+  actions.user.remove= function(req, res) {
+    // 未実装
+    return res.redirect('/admin/users');
+  };
+
+  actions.user.removeCompletely = function(req, res) {
+    // ユーザーの物理削除
+    var id = req.params.id;
+
+    User.removeCompletelyById(id, function(err, removed) {
+      if (err) {
+        debug('Error while removing user.', err, id);
+        req.flash('errorMessage', '完全な削除に失敗しました。');
+      } else {
+        req.flash('successMessage', '削除しました');
+      }
+      return res.redirect('/admin/users');
+    });
+  };
+
   actions.api = {};
   actions.api.appSetting = function(req, res) {
     var form = req.form.settingForm;

+ 3 - 0
routes/index.js

@@ -37,10 +37,13 @@ module.exports = function(app) {
   , admin.api.appSetting);
 
   app.get('/admin/users'                , middleware.loginRequired(app) , middleware.adminRequired() , admin.user.index);
+  app.post('/admin/user/invite'         , form.admin.userInvite ,  middleware.loginRequired(app) , middleware.adminRequired() , admin.user.invite);
   app.post('/admin/user/:id/makeAdmin'  , middleware.loginRequired(app) , middleware.adminRequired() , admin.user.makeAdmin);
   app.post('/admin/user/:id/removeFromAdmin', middleware.loginRequired(app) , middleware.adminRequired() , admin.user.removeFromAdmin);
   app.post('/admin/user/:id/activate'   , middleware.loginRequired(app) , middleware.adminRequired() , admin.user.activate);
   app.post('/admin/user/:id/suspend'    , middleware.loginRequired(app) , middleware.adminRequired() , admin.user.suspend);
+  app.post('/admin/user/:id/remove'     , middleware.loginRequired(app) , middleware.adminRequired() , admin.user.remove);
+  app.post('/admin/user/:id/removeCompletely' , middleware.loginRequired(app) , middleware.adminRequired() , admin.user.removeCompletely);
 
   app.get('/me'                      , middleware.loginRequired(app) , me.index);
   app.get('/me/password'             , middleware.loginRequired(app) , me.password);

+ 58 - 3
views/admin/users.html

@@ -32,7 +32,54 @@
         <li class="active"><a href="/admin/users"><i class="fa fa-users"></i> ユーザー管理</a></li>
       </ul>
     </div>
+
     <div class="col-md-9">
+      <p>
+        <button  data-toggle="collapse" class="btn btn-default" href="#inviteUserForm">新規ユーザーの招待</button>
+      </p>
+      <form role="form" action="/admin/user/invite" method="post">
+        <div id="inviteUserForm" class="collapse">
+          <div class="form-group">
+            <label for="inviteForm[emailList]">メールアドレス (複数行入力で複数人招待可能)</label>
+            <textarea class="form-control" name="inviteForm[emailList]" placeholder="例: user@crowi.wiki"></textarea>
+          </div>
+          <div class="checkbox">
+            <label>
+              <input type="checkbox" name="inviteForm[sendEmail]" checked> 招待をメールで送信
+            </label>
+          </div>
+          <button type="submit" class="btn btn-primary">招待する</button>
+        </div>
+      </form>
+
+      {% set createdUser = req.flash('createdUser') %}
+      {% if createdUser.length %}
+      <div class="modal fade in" id="createdUserModal">
+        <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>
+
+              <pre>{% for cUser in createdUser %}{% if cUser.user %}{{ cUser.email }} {{ cUser.password }}<br>{% else %}{{ cUser.email }} 作成失敗<br>{% endif %}{% endfor %}</pre>
+            </div>
+
+          </div><!-- /.modal-content -->
+        </div><!-- /.modal-dialog -->
+      </div><!-- /.modal -->
+      <script>$(function() { $('#createdUserModal').modal('show'); });</script>
+      {% endif %}
+
+      <h2>ユーザー一覧</h2>
+
       <table class="table table-hover table-striped table-bordered">
         <thead>
           <tr>
@@ -96,10 +143,18 @@
                   </form>
                   </li>
                   <li class="dropdown-button">
-                  <form action="/admin/user/{{ sUser._id.toString() }}/suspend" method="post">
-                    <button type="submit" class="btn btn-block btn-danger">完全に削除する</button>
+                  {# label は同じだけど、こっちは論理削除 #}
+                  <form action="/admin/user/{{ sUser._id.toString() }}/remove" method="post">
+                    <button type="submit" class="btn btn-block btn-danger">削除する</button>
                   </form>
                   {% endif  %}
+                  {% if sUser.status == 5 %}
+                  {# label は同じだけど、こっちは物理削除 #}
+                  <form action="/admin/user/{{ sUser._id.toString() }}/removeCompletely" method="post">
+                    <button type="submit" class="btn btn-block btn-danger">削除する</button>
+                  </form>
+                  {% endif  %}
+                  </li>
 
                   {% if sUser.status == 2 %} {# activated な人だけこのメニューを表示 #}
                   <li class="divider"></li>
@@ -116,7 +171,7 @@
                       {% endif %}
                     {% else %}
                       <form action="/admin/user/{{ sUser._id.toString() }}/makeAdmin" method="post">
-                        <button type="submit" class="btn btn-block btn-danger">管理者にする</button>
+                        <button type="submit" class="btn btn-block btn-primary">管理者にする</button>
                       </form>
                     {% endif %}
                   </li>