Browse Source

Merge branch 'devel' of https://github.com/thuanpq/wekan into thuanpq-devel

Lauri Ojansivu 7 years ago
parent
commit
22e43e6591

+ 1 - 0
.gitignore

@@ -5,6 +5,7 @@
 tmp/
 node_modules/
 .vscode/
+.idea/
 .build/*
 packages/kadira-flow-router/
 packages/meteor-useraccounts-core/

+ 89 - 0
client/components/settings/peopleBody.jade

@@ -0,0 +1,89 @@
+template(name="people")
+  .setting-content
+    unless currentUser.isAdmin
+      | {{_ 'error-notAuthorized'}}
+    else
+      .content-title
+        span {{_ 'people'}}
+      .content-body
+        .side-menu
+          ul
+            li.active
+              a.js-setting-menu(data-id="people-setting") {{_ 'people'}}
+        .main-body
+          if loading.get
+            +spinner
+          else if people.get
+            +peopleGeneral
+
+template(name="peopleGeneral")
+  table
+    tbody
+      tr
+        th {{_ 'username'}}
+        th {{_ 'fullname'}}
+        th {{_ 'isAdmin'}}
+        th {{_ 'email'}}
+        th {{_ 'verified'}}
+        th {{_ 'createdAt'}}
+        th {{_ 'active'}}
+        th
+      each user in peopleList
+        +peopleRow(userId=user._id)
+
+template(name="peopleRow")
+  tr
+    td.username {{ userData.username }}
+    td {{ userData.profile.fullname }}
+    td
+      if userData.isAdmin
+        | true
+      else
+        | false
+    td {{ userData.emails.[0].address }}
+    td
+      if userData.emails.[0].verified
+        | true
+      else
+        | false
+    td {{ moment userData.createdAt 'LLL' }}
+    td
+      if userData.loginDisabled
+        | false
+      else
+        | true
+    td
+      a.edit-user
+        | edit
+
+template(name="editUserPopup")
+  form
+    label.hide.userId(type="text" value=user._id)
+    label
+      | {{_ 'fullname'}}
+      input.js-profile-fullname(type="text" value=user.profile.fullname autofocus)
+    label
+      | {{_ 'username'}}
+      span.error.hide.username-taken
+        | {{_ 'error-username-taken'}}
+      input.js-profile-username(type="text" value=user.username)
+    label
+      | {{_ 'initials'}}
+      input.js-profile-initials(type="text" value=user.profile.initials)
+    label
+      | {{_ 'email'}}
+      span.error.hide.email-taken
+        | {{_ 'error-email-taken'}}
+      input.js-profile-email(type="email" value="{{user.emails.[0].address}}")
+    label
+      | {{_ 'isAdmin'}}
+      select.select-role.js-profile-isadmin
+        option(value="false") No
+        option(value="true" selected="{{user.isAdmin}}") Yes
+    label
+      | {{_ 'isActive'}}
+      select.select-active.js-profile-isactive
+        option(value="false") Yes
+        option(value="true" selected="{{user.loginDisabled}}") No
+
+    input.primary.wide(type="submit" value="{{_ 'save'}}")

+ 156 - 0
client/components/settings/peopleBody.js

@@ -0,0 +1,156 @@
+const usersPerPage = 25;
+
+BlazeComponent.extendComponent({
+  mixins() {
+    return [Mixins.InfiniteScrolling];
+  },
+  onCreated() {
+    this.error = new ReactiveVar('');
+    this.loading = new ReactiveVar(false);
+    this.people = new ReactiveVar(true);
+
+    this.page = new ReactiveVar(1);
+    this.loadNextPageLocked = false;
+    this.callFirstWith(null, 'resetNextPeak');
+    this.autorun(() => {
+      const limit = this.page.get() * usersPerPage;
+
+      this.subscribe('people', limit, () => {
+        this.loadNextPageLocked = false;
+        const nextPeakBefore = this.callFirstWith(null, 'getNextPeak');
+        this.calculateNextPeak();
+        const nextPeakAfter = this.callFirstWith(null, 'getNextPeak');
+        if (nextPeakBefore === nextPeakAfter) {
+          this.callFirstWith(null, 'resetNextPeak');
+        }
+      });
+    });
+  },
+  loadNextPage() {
+    if (this.loadNextPageLocked === false) {
+      this.page.set(this.page.get() + 1);
+      this.loadNextPageLocked = true;
+    }
+  },
+  calculateNextPeak() {
+    const element = this.find('.main-body');
+    if (element) {
+      const altitude = element.scrollHeight;
+      this.callFirstWith(this, 'setNextPeak', altitude);
+    }
+  },
+  reachNextPeak() {
+    this.loadNextPage();
+  },
+  setError(error) {
+    this.error.set(error);
+  },
+  setLoading(w) {
+    this.loading.set(w);
+  },
+  peopleList() {
+    return Users.find({}, {
+      fields: {_id: true},
+    });
+  },
+}).register('people');
+
+Template.peopleRow.helpers({
+  userData() {
+    const userCollection = this.esSearch ? ESSearchResults : Users;
+    return userCollection.findOne(this.userId);
+  },
+});
+
+Template.editUserPopup.helpers({
+  user() {
+    return Users.findOne(this.userId);
+  },
+});
+
+BlazeComponent.extendComponent({
+  onCreated() {
+  },
+  user() {
+    return Users.findOne(this.userId);
+  },
+  events() {
+    return [{
+      'click a.edit-user': Popup.open('editUser'),
+    }];
+  },
+}).register('peopleRow');
+
+Template.editUserPopup.events({
+  submit(evt, tpl) {
+    evt.preventDefault();
+    const user = Users.findOne(this.userId);
+    const fullname = tpl.find('.js-profile-fullname').value.trim();
+    const username = tpl.find('.js-profile-username').value.trim();
+    const initials = tpl.find('.js-profile-initials').value.trim();
+    const isAdmin = tpl.find('.js-profile-isadmin').value.trim();
+    const isActive = tpl.find('.js-profile-isactive').value.trim();
+    const email = tpl.find('.js-profile-email').value.trim();
+    let isChangeUserName = false;
+    let isChangeEmail = false;
+
+    Users.update(this.userId, {
+      $set: {
+        'profile.fullname': fullname,
+        'profile.initials': initials,
+        'isAdmin': isAdmin === 'true',
+        'loginDisabled': isActive === 'true',
+      },
+    });
+
+    isChangeUserName = username !== user.username;
+    isChangeEmail = email.toLowerCase() !== user.emails[0].address.toLowerCase();
+
+    if (isChangeUserName && isChangeEmail) {
+      Meteor.call('setUsernameAndEmail', username, email.toLowerCase(), this.userId, function (error) {
+        const usernameMessageElement = tpl.$('.username-taken');
+        const emailMessageElement = tpl.$('.email-taken');
+        if (error) {
+          const errorElement = error.error;
+          if (errorElement === 'username-already-taken') {
+            usernameMessageElement.show();
+            emailMessageElement.hide();
+          } else if (errorElement === 'email-already-taken') {
+            usernameMessageElement.hide();
+            emailMessageElement.show();
+          }
+        } else {
+          usernameMessageElement.hide();
+          emailMessageElement.hide();
+          Popup.close();
+        }
+      });
+    } else if (isChangeUserName) {
+      Meteor.call('setUsername', username, this.userId, function (error) {
+        const usernameMessageElement = tpl.$('.username-taken');
+        if (error) {
+          const errorElement = error.error;
+          if (errorElement === 'username-already-taken') {
+            usernameMessageElement.show();
+          }
+        } else {
+          usernameMessageElement.hide();
+          Popup.close();
+        }
+      });
+    } else if (isChangeEmail) {
+      Meteor.call('setEmail', email.toLowerCase(), this.userId, function (error) {
+        const emailMessageElement = tpl.$('.email-taken');
+        if (error) {
+          const errorElement = error.error;
+          if (errorElement === 'email-already-taken') {
+            emailMessageElement.show();
+          }
+        } else {
+          emailMessageElement.hide();
+          Popup.close();
+        }
+      });
+    } else Popup.close();
+  },
+});

+ 15 - 0
client/components/settings/peopleBody.styl

@@ -0,0 +1,15 @@
+.main-body
+  overflow: scroll;
+
+table
+  font-family: arial, sans-serif;
+  border-collapse: collapse;
+  width: 100%;
+
+  td, th
+    border: 1px solid #d2d0d0;
+    text-align: left;
+    padding: 8px;
+
+  tr:nth-child(even)
+    background-color: #dddddd;

+ 5 - 4
client/components/settings/settingHeader.jade

@@ -9,13 +9,14 @@ template(name="settingHeaderBar")
           a.setting-header-btn.settings(href="{{pathFor 'setting'}}")
             i.fa(class="fa-cog")
             span {{_ 'settings'}}
+
           a.setting-header-btn.informations(href="{{pathFor 'information'}}")
             i.fa(class="fa-info-circle")
             span {{_ 'info'}}
-//TODO
-//          a.setting-header-btn.people
-//            i.fa(class="fa-users")
-//            span {{_ 'people'}}
+
+          a.setting-header-btn.people(href="{{pathFor 'people'}}")
+            i.fa(class="fa-users")
+            span {{_ 'people'}}
 
         else
           a.setting-header-btn.js-log-in(

+ 5 - 5
client/components/users/userHeader.js

@@ -42,7 +42,7 @@ Template.editProfilePopup.events({
     isChangeUserName = username !== Meteor.user().username;
     isChangeEmail = email.toLowerCase() !== Meteor.user().emails[0].address.toLowerCase();
     if (isChangeUserName && isChangeEmail) {
-      Meteor.call('setUsernameAndEmail', username, email.toLowerCase(), function(error) {
+      Meteor.call('setUsernameAndEmail', username, email.toLowerCase(), Meteor.userId(), function (error) {
         const usernameMessageElement = tpl.$('.username-taken');
         const emailMessageElement = tpl.$('.email-taken');
         if (error) {
@@ -61,7 +61,7 @@ Template.editProfilePopup.events({
         }
       });
     } else if (isChangeUserName) {
-      Meteor.call('setUsername', username, function(error) {
+      Meteor.call('setUsername', username, Meteor.userId(), function (error) {
         const messageElement = tpl.$('.username-taken');
         if (error) {
           messageElement.show();
@@ -71,7 +71,7 @@ Template.editProfilePopup.events({
         }
       });
     } else if (isChangeEmail) {
-      Meteor.call('setEmail', email.toLowerCase(), function(error) {
+      Meteor.call('setEmail', email.toLowerCase(), Meteor.userId(), function (error) {
         const messageElement = tpl.$('.email-taken');
         if (error) {
           messageElement.show();
@@ -105,7 +105,7 @@ Template.editNotificationPopup.events({
 
 // XXX For some reason the useraccounts autofocus isnt working in this case.
 // See https://github.com/meteor-useraccounts/core/issues/384
-Template.changePasswordPopup.onRendered(function() {
+Template.changePasswordPopup.onRendered(function () {
   this.find('#at-field-current_password').focus();
 });
 
@@ -116,7 +116,7 @@ Template.changeLanguagePopup.helpers({
         tag: code,
         name: lang.name === 'br' ? 'Brezhoneg' : lang.name,
       };
-    }).sort(function(a, b) {
+    }).sort(function (a, b) {
       if (a.name === b.name) {
         return 0;
       } else {

+ 20 - 0
config/router.js

@@ -140,6 +140,26 @@ FlowRouter.route('/information', {
   },
 });
 
+FlowRouter.route('/people', {
+  name: 'people',
+  triggersEnter: [
+    AccountsTemplates.ensureSignedIn,
+    () => {
+      Session.set('currentBoard', null);
+      Session.set('currentCard', null);
+
+      Filter.reset();
+      EscapeActions.executeAll();
+    },
+  ],
+  action() {
+    BlazeLayout.render('defaultLayout', {
+      headerBar: 'settingHeaderBar',
+      content: 'people',
+    });
+  },
+});
+
 FlowRouter.notFound = {
   action() {
     BlazeLayout.render('defaultLayout', { content: 'notFound' });

+ 65 - 51
models/users.js

@@ -118,6 +118,13 @@ Users.attachSchema(new SimpleSchema({
   },
 }));
 
+Users.allow({
+  update(userId) {
+    const user = Users.findOne(userId);
+    return user && Meteor.user().isAdmin;
+  },
+});
+
 // Search a user in the complete server database by its name or username. This
 // is used for instance to add a new user to a board.
 const searchInFields = ['username', 'profile.fullname'];
@@ -152,36 +159,36 @@ if (Meteor.isClient) {
 
 Users.helpers({
   boards() {
-    return Boards.find({ userId: this._id });
+    return Boards.find({userId: this._id});
   },
 
   starredBoards() {
-    const { starredBoards = [] } = this.profile;
-    return Boards.find({ archived: false, _id: { $in: starredBoards } });
+    const {starredBoards = []} = this.profile;
+    return Boards.find({archived: false, _id: {$in: starredBoards}});
   },
 
   hasStarred(boardId) {
-    const { starredBoards = [] } = this.profile;
+    const {starredBoards = []} = this.profile;
     return _.contains(starredBoards, boardId);
   },
 
   invitedBoards() {
-    const { invitedBoards = [] } = this.profile;
-    return Boards.find({ archived: false, _id: { $in: invitedBoards } });
+    const {invitedBoards = []} = this.profile;
+    return Boards.find({archived: false, _id: {$in: invitedBoards}});
   },
 
   isInvitedTo(boardId) {
-    const { invitedBoards = [] } = this.profile;
+    const {invitedBoards = []} = this.profile;
     return _.contains(invitedBoards, boardId);
   },
 
   hasTag(tag) {
-    const { tags = [] } = this.profile;
+    const {tags = []} = this.profile;
     return _.contains(tags, tag);
   },
 
   hasNotification(activityId) {
-    const { notifications = [] } = this.profile;
+    const {notifications = []} = this.profile;
     return _.contains(notifications, activityId);
   },
 
@@ -191,7 +198,7 @@ Users.helpers({
   },
 
   getEmailBuffer() {
-    const { emailBuffer = [] } = this.profile;
+    const {emailBuffer = []} = this.profile;
     return emailBuffer;
   },
 
@@ -316,22 +323,22 @@ Users.mutations({
   },
 
   setAvatarUrl(avatarUrl) {
-    return { $set: { 'profile.avatarUrl': avatarUrl } };
+    return {$set: {'profile.avatarUrl': avatarUrl}};
   },
 
   setShowCardsCountAt(limit) {
-    return { $set: { 'profile.showCardsCountAt': limit } };
+    return {$set: {'profile.showCardsCountAt': limit}};
   },
 });
 
 Meteor.methods({
-  setUsername(username) {
+  setUsername(username, userId) {
     check(username, String);
-    const nUsersWithUsername = Users.find({ username }).count();
+    const nUsersWithUsername = Users.find({username}).count();
     if (nUsersWithUsername > 0) {
       throw new Meteor.Error('username-already-taken');
     } else {
-      Users.update(this.userId, { $set: { username } });
+      Users.update(userId, {$set: {username}});
     }
   },
   toggleSystemMessages() {
@@ -342,13 +349,13 @@ Meteor.methods({
     check(limit, Number);
     Meteor.user().setShowCardsCountAt(limit);
   },
-  setEmail(email) {
+  setEmail(email, userId) {
     check(email, String);
-    const existingUser = Users.findOne({ 'emails.address': email }, { fields: { _id: 1 } });
+    const existingUser = Users.findOne({'emails.address': email}, {fields: {_id: 1}});
     if (existingUser) {
       throw new Meteor.Error('email-already-taken');
     } else {
-      Users.update(this.userId, {
+      Users.update(userId, {
         $set: {
           emails: [{
             address: email,
@@ -358,11 +365,12 @@ Meteor.methods({
       });
     }
   },
-  setUsernameAndEmail(username, email) {
+  setUsernameAndEmail(username, email, userId) {
     check(username, String);
     check(email, String);
-    Meteor.call('setUsername', username);
-    Meteor.call('setEmail', email);
+    check(userId, String);
+    Meteor.call('setUsername', username, userId);
+    Meteor.call('setEmail', email, userId);
   },
 });
 
@@ -379,8 +387,8 @@ if (Meteor.isServer) {
         board &&
         board.members &&
         _.contains(_.pluck(board.members, 'userId'), inviter._id) &&
-        _.where(board.members, { userId: inviter._id })[0].isActive &&
-        _.where(board.members, { userId: inviter._id })[0].isAdmin;
+        _.where(board.members, {userId: inviter._id})[0].isActive &&
+        _.where(board.members, {userId: inviter._id})[0].isAdmin;
       if (!allowInvite) throw new Meteor.Error('error-board-notAMember');
 
       this.unblock();
@@ -388,9 +396,9 @@ if (Meteor.isServer) {
       const posAt = username.indexOf('@');
       let user = null;
       if (posAt >= 0) {
-        user = Users.findOne({ emails: { $elemMatch: { address: username } } });
+        user = Users.findOne({emails: {$elemMatch: {address: username}}});
       } else {
-        user = Users.findOne(username) || Users.findOne({ username });
+        user = Users.findOne(username) || Users.findOne({username});
       }
       if (user) {
         if (user._id === inviter._id) throw new Meteor.Error('error-user-notAllowSelf');
@@ -400,7 +408,7 @@ if (Meteor.isServer) {
         // Set in lowercase email before creating account
         const email = username.toLowerCase();
         username = email.substring(0, posAt);
-        const newUserId = Accounts.createUser({ username, email });
+        const newUserId = Accounts.createUser({username, email});
         if (!newUserId) throw new Meteor.Error('error-user-notCreated');
         // assume new user speak same language with inviter
         if (inviter.profile && inviter.profile.language) {
@@ -434,7 +442,7 @@ if (Meteor.isServer) {
       } catch (e) {
         throw new Meteor.Error('email-fail', e.message);
       }
-      return { username: user.username, email: user.emails[0].address };
+      return {username: user.username, email: user.emails[0].address};
     },
   });
   Accounts.onCreateUser((options, user) => {
@@ -457,11 +465,15 @@ if (Meteor.isServer) {
     if (!options || !options.profile) {
       throw new Meteor.Error('error-invitation-code-blank', 'The invitation code is required');
     }
-    const invitationCode = InvitationCodes.findOne({ code: options.profile.invitationcode, email: options.email, valid: true });
+    const invitationCode = InvitationCodes.findOne({
+      code: options.profile.invitationcode,
+      email: options.email,
+      valid: true,
+    });
     if (!invitationCode) {
       throw new Meteor.Error('error-invitation-code-not-exist', 'The invitation code doesn\'t exist');
     } else {
-      user.profile = { icode: options.profile.invitationcode };
+      user.profile = {icode: options.profile.invitationcode};
     }
 
     return user;
@@ -473,7 +485,7 @@ if (Meteor.isServer) {
   Meteor.startup(() => {
     Users._collection._ensureIndex({
       username: 1,
-    }, { unique: true });
+    }, {unique: true});
   });
 
   // Each board document contains the de-normalized number of users that have
@@ -492,6 +504,7 @@ if (Meteor.isServer) {
     function getStarredBoardsIds(doc) {
       return doc.profile && doc.profile.starredBoards;
     }
+
     const oldIds = getStarredBoardsIds(this.previous);
     const newIds = getStarredBoardsIds(user);
 
@@ -500,9 +513,10 @@ if (Meteor.isServer) {
     // direction and then in the other.
     function incrementBoards(boardsIds, inc) {
       boardsIds.forEach((boardId) => {
-        Boards.update(boardId, { $inc: { stars: inc } });
+        Boards.update(boardId, {$inc: {stars: inc}});
       });
     }
+
     incrementBoards(_.difference(oldIds, newIds), -1);
     incrementBoards(_.difference(newIds, oldIds), +1);
   });
@@ -529,7 +543,7 @@ if (Meteor.isServer) {
         }, fakeUser, (err, boardId) => {
 
           ['welcome-list1', 'welcome-list2'].forEach((title) => {
-            Lists.insert({ title: TAPi18n.__(title), boardId }, fakeUser);
+            Lists.insert({title: TAPi18n.__(title), boardId}, fakeUser);
           });
         });
       });
@@ -545,14 +559,14 @@ if (Meteor.isServer) {
       // the disableRegistration check.
       // Issue : https://github.com/wekan/wekan/issues/1232
       // PR    : https://github.com/wekan/wekan/pull/1251
-      Users.update(doc._id, { $set: { createdThroughApi: '' } });
+      Users.update(doc._id, {$set: {createdThroughApi: ''}});
       return;
     }
 
     //invite user to corresponding boards
     const disableRegistration = Settings.findOne().disableRegistration;
     if (disableRegistration) {
-      const invitationCode = InvitationCodes.findOne({ code: doc.profile.icode, valid: true });
+      const invitationCode = InvitationCodes.findOne({code: doc.profile.icode, valid: true});
       if (!invitationCode) {
         throw new Meteor.Error('error-invitation-code-not-exist');
       } else {
@@ -564,8 +578,8 @@ if (Meteor.isServer) {
           doc.profile = {};
         }
         doc.profile.invitedBoards = invitationCode.boardsToBeInvited;
-        Users.update(doc._id, { $set: { profile: doc.profile } });
-        InvitationCodes.update(invitationCode._id, { $set: { valid: false } });
+        Users.update(doc._id, {$set: {profile: doc.profile}});
+        InvitationCodes.update(invitationCode._id, {$set: {valid: false}});
       }
     }
   });
@@ -574,9 +588,9 @@ if (Meteor.isServer) {
 
 // USERS REST API
 if (Meteor.isServer) {
-  JsonRoutes.add('GET', '/api/user', function(req, res, next) {
+  JsonRoutes.add('GET', '/api/user', function (req, res, next) {
     Authentication.checkLoggedIn(req.userId);
-    const data = Meteor.users.findOne({ _id: req.userId});
+    const data = Meteor.users.findOne({_id: req.userId});
     delete data.services;
     JsonRoutes.sendResult(res, {
       code: 200,
@@ -585,33 +599,33 @@ if (Meteor.isServer) {
   });
 
   JsonRoutes.add('GET', '/api/users', function (req, res, next) {
-    Authentication.checkUserId( req.userId);
+    Authentication.checkUserId(req.userId);
     JsonRoutes.sendResult(res, {
       code: 200,
       data: Meteor.users.find({}).map(function (doc) {
-        return { _id: doc._id, username: doc.username };
+        return {_id: doc._id, username: doc.username};
       }),
     });
   });
   JsonRoutes.add('GET', '/api/users/:id', function (req, res, next) {
-    Authentication.checkUserId( req.userId);
+    Authentication.checkUserId(req.userId);
     const id = req.params.id;
     JsonRoutes.sendResult(res, {
       code: 200,
-      data: Meteor.users.findOne({ _id: id }),
+      data: Meteor.users.findOne({_id: id}),
     });
   });
   JsonRoutes.add('PUT', '/api/users/:id', function (req, res, next) {
-    Authentication.checkUserId( req.userId);
+    Authentication.checkUserId(req.userId);
     const id = req.params.id;
     const action = req.body.action;
-    let data = Meteor.users.findOne({ _id: id });
+    let data = Meteor.users.findOne({_id: id});
     if (data !== undefined) {
       if (action === 'takeOwnership') {
         data = Boards.find({
           'members.userId': id,
           'members.isAdmin': true,
-        }).map(function(board) {
+        }).map(function (board) {
           if (board.hasMember(req.userId)) {
             board.removeMember(req.userId);
           }
@@ -623,11 +637,11 @@ if (Meteor.isServer) {
         });
       } else {
         if ((action === 'disableLogin') && (id !== req.userId)) {
-          Users.update({ _id: id }, { $set: { loginDisabled: true, 'services.resume.loginTokens': '' } });
+          Users.update({_id: id}, {$set: {loginDisabled: true, 'services.resume.loginTokens': ''}});
         } else if (action === 'enableLogin') {
-          Users.update({ _id: id }, { $set: { loginDisabled: '' } });
+          Users.update({_id: id}, {$set: {loginDisabled: ''}});
         }
-        data = Meteor.users.findOne({ _id: id });
+        data = Meteor.users.findOne({_id: id});
       }
     }
     JsonRoutes.sendResult(res, {
@@ -636,7 +650,7 @@ if (Meteor.isServer) {
     });
   });
   JsonRoutes.add('POST', '/api/users/', function (req, res, next) {
-    Authentication.checkUserId( req.userId);
+    Authentication.checkUserId(req.userId);
     const id = Accounts.createUser({
       username: req.body.username,
       email: req.body.email,
@@ -653,9 +667,9 @@ if (Meteor.isServer) {
   });
 
   JsonRoutes.add('DELETE', '/api/users/:id', function (req, res, next) {
-    Authentication.checkUserId( req.userId);
+    Authentication.checkUserId(req.userId);
     const id = req.params.id;
-    Meteor.users.remove({ _id: id });
+    Meteor.users.remove({_id: id});
     JsonRoutes.sendResult(res, {
       code: 200,
       data: {

+ 7 - 0
server/publications/people.js

@@ -0,0 +1,7 @@
+Meteor.publish('people', (limit) => {
+  check(limit, Number);
+  return Users.find({}, {
+    limit,
+    sort: {createdAt: -1},
+  });
+});