Bladeren bron

Merge pull request #1294 from weseek/imprv/admin-user-management

Imprv/admin user management
Yuki Takei 6 jaren geleden
bovenliggende
commit
3f502b81e0

+ 2 - 2
package.json

@@ -20,7 +20,7 @@
     "url": "https://github.com/weseek/growi/issues"
     "url": "https://github.com/weseek/growi/issues"
   },
   },
   "scripts": {
   "scripts": {
-    "build:apiv3:jsdoc": "swagger-jsdoc -o tmp/swagger.json -d config/swagger-definition.js src/server/routes/apiv3/**/*.js",
+    "build:apiv3:jsdoc": "swagger-jsdoc -o tmp/swagger.json -d config/swagger-definition.js src/server/**/*.js",
     "build:dev:app:watch": "npm run build:dev:app -- --watch",
     "build:dev:app:watch": "npm run build:dev:app -- --watch",
     "build:dev:app": "env-cmd -f config/env.dev.js webpack --config config/webpack.dev.js --progress",
     "build:dev:app": "env-cmd -f config/env.dev.js webpack --config config/webpack.dev.js --progress",
     "build:dev:dll": "webpack --config config/webpack.dev.dll.js",
     "build:dev:dll": "webpack --config config/webpack.dev.dll.js",
@@ -114,7 +114,7 @@
     "module-alias": "^2.0.6",
     "module-alias": "^2.0.6",
     "mongoose": "5.4.4",
     "mongoose": "5.4.4",
     "mongoose-gridfs": "^1.2.2",
     "mongoose-gridfs": "^1.2.2",
-    "mongoose-paginate": "^5.0.3",
+    "mongoose-paginate-v2": "^1.3.2",
     "mongoose-unique-validator": "^2.0.3",
     "mongoose-unique-validator": "^2.0.3",
     "multer": "~1.4.0",
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "multer-autoreap": "^1.0.3",

+ 2 - 2
src/client/js/app.jsx

@@ -39,7 +39,7 @@ import CustomCssEditor from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
 import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
-import Users from './components/Admin/Users/Users';
+import UserManagement from './components/Admin/UserManagement';
 import ManageExternalAccount from './components/Admin/Users/ManageExternalAccount';
 import ManageExternalAccount from './components/Admin/Users/ManageExternalAccount';
 import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
 import UserGroupPage from './components/Admin/UserGroup/UserGroupPage';
 import Customize from './components/Admin/Customize/Customize';
 import Customize from './components/Admin/Customize/Customize';
@@ -164,7 +164,7 @@ if (adminUsersElem != null) {
   ReactDOM.render(
   ReactDOM.render(
     <Provider inject={[injectableContainers, adminUsersContainer]}>
     <Provider inject={[injectableContainers, adminUsersContainer]}>
       <I18nextProvider i18n={i18n}>
       <I18nextProvider i18n={i18n}>
-        <Users />
+        <UserManagement />
       </I18nextProvider>
       </I18nextProvider>
     </Provider>,
     </Provider>,
     adminUsersElem,
     adminUsersElem,

+ 37 - 20
src/client/js/components/Admin/Users/Users.jsx → src/client/js/components/Admin/UserManagement.jsx

@@ -2,18 +2,20 @@ import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import PasswordResetModal from './PasswordResetModal';
-import PaginationWrapper from '../../PaginationWrapper';
-import InviteUserControl from './InviteUserControl';
-import UserTable from './UserTable';
+import PaginationWrapper from '../PaginationWrapper';
 
 
-import { createSubscribedElement } from '../../UnstatedUtils';
-import { toastError } from '../../../util/apiNotification';
 
 
-import AppContainer from '../../../services/AppContainer';
-import AdminUsersContainer from '../../../services/AdminUsersContainer';
+import { createSubscribedElement } from '../UnstatedUtils';
+import { toastError } from '../../util/apiNotification';
 
 
-class UserPage extends React.Component {
+import AppContainer from '../../services/AppContainer';
+import AdminUsersContainer from '../../services/AdminUsersContainer';
+
+import PasswordResetModal from './Users/PasswordResetModal';
+import InviteUserControl from './Users/InviteUserControl';
+import UserTable from './Users/UserTable';
+
+class UserManagement extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super();
     super();
@@ -21,6 +23,10 @@ class UserPage extends React.Component {
     this.handlePage = this.handlePage.bind(this);
     this.handlePage = this.handlePage.bind(this);
   }
   }
 
 
+  componentWillMount() {
+    this.handlePage(1);
+  }
+
   async handlePage(selectedPage) {
   async handlePage(selectedPage) {
     try {
     try {
       await this.props.adminUsersContainer.retrieveUsersByPagingNum(selectedPage);
       await this.props.adminUsersContainer.retrieveUsersByPagingNum(selectedPage);
@@ -33,6 +39,17 @@ class UserPage extends React.Component {
   render() {
   render() {
     const { t, adminUsersContainer } = this.props;
     const { t, adminUsersContainer } = this.props;
 
 
+    const pager = (
+      <div className="pull-right">
+        <PaginationWrapper
+          activePage={adminUsersContainer.state.activePage}
+          changePage={this.handlePage}
+          totalItemsCount={adminUsersContainer.state.totalUsers}
+          pagingLimit={adminUsersContainer.state.pagingLimit}
+        />
+      </div>
+    );
+
     return (
     return (
       <Fragment>
       <Fragment>
         {adminUsersContainer.state.userForPasswordResetModal && <PasswordResetModal />}
         {adminUsersContainer.state.userForPasswordResetModal && <PasswordResetModal />}
@@ -43,28 +60,28 @@ class UserPage extends React.Component {
             { t('user_management.external_account') }
             { t('user_management.external_account') }
           </a>
           </a>
         </p>
         </p>
+
+        <h2>{ t('User_Management') }</h2>
+
+        {pager}
         <UserTable />
         <UserTable />
-        <PaginationWrapper
-          activePage={adminUsersContainer.state.activePage}
-          changePage={this.handlePage}
-          totalItemsCount={adminUsersContainer.state.totalUsers}
-          pagingLimit={adminUsersContainer.state.pagingLimit}
-        />
+        {pager}
+
       </Fragment>
       </Fragment>
     );
     );
   }
   }
 
 
 }
 }
 
 
-const UserPageWrapper = (props) => {
-  return createSubscribedElement(UserPage, props, [AppContainer, AdminUsersContainer]);
-};
 
 
-UserPage.propTypes = {
+UserManagement.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
   adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
+};
 
 
+const UserManagementWrapper = (props) => {
+  return createSubscribedElement(UserManagement, props, [AppContainer, AdminUsersContainer]);
 };
 };
 
 
-export default withTranslation()(UserPageWrapper);
+export default withTranslation()(UserManagementWrapper);

+ 1 - 3
src/client/js/components/Admin/Users/UserTable.jsx

@@ -66,14 +66,12 @@ class UserTable extends React.Component {
 
 
     return (
     return (
       <Fragment>
       <Fragment>
-        <h2>{ t('User_Management') }</h2>
-
         <table className="table table-default table-bordered table-user-list">
         <table className="table table-default table-bordered table-user-list">
           <thead>
           <thead>
             <tr>
             <tr>
               <th width="100px">#</th>
               <th width="100px">#</th>
               <th>{ t('status') }</th>
               <th>{ t('status') }</th>
-              <th><code>{ t('User') }</code></th>
+              <th><code>username</code></th>
               <th>{ t('Name') }</th>
               <th>{ t('Name') }</th>
               <th>{ t('Email') }</th>
               <th>{ t('Email') }</th>
               <th width="100px">{ t('Created') }</th>
               <th width="100px">{ t('Created') }</th>

+ 7 - 5
src/client/js/services/AdminUsersContainer.js

@@ -17,7 +17,7 @@ export default class AdminUsersContainer extends Container {
     this.appContainer = appContainer;
     this.appContainer = appContainer;
 
 
     this.state = {
     this.state = {
-      users: JSON.parse(document.getElementById('admin-user-page').getAttribute('users')) || [],
+      users: [],
       isPasswordResetModalShown: false,
       isPasswordResetModalShown: false,
       isUserInviteModalShown: false,
       isUserInviteModalShown: false,
       userForPasswordResetModal: null,
       userForPasswordResetModal: null,
@@ -46,11 +46,13 @@ export default class AdminUsersContainer extends Container {
   async retrieveUsersByPagingNum(selectedPage) {
   async retrieveUsersByPagingNum(selectedPage) {
 
 
     const params = { page: selectedPage };
     const params = { page: selectedPage };
-    const response = await this.appContainer.apiv3.get('/users', params);
+    const { data } = await this.appContainer.apiv3.get('/users', params);
 
 
-    const users = response.data.users;
-    const totalUsers = response.data.totalUsers;
-    const pagingLimit = response.data.pagingLimit;
+    if (data.paginateResult == null) {
+      throw new Error('data must conclude \'paginateResult\' property.');
+    }
+
+    const { docs: users, totalDocs: totalUsers, limit: pagingLimit } = data.paginateResult;
 
 
     this.setState({
     this.setState({
       users,
       users,

+ 1 - 1
src/server/models/external-account.js

@@ -3,7 +3,7 @@
 
 
 const debug = require('debug')('growi:models:external-account');
 const debug = require('debug')('growi:models:external-account');
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
-const mongoosePaginate = require('mongoose-paginate');
+const mongoosePaginate = require('mongoose-paginate-v2');
 const uniqueValidator = require('mongoose-unique-validator');
 const uniqueValidator = require('mongoose-unique-validator');
 
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
 const ObjectId = mongoose.Schema.Types.ObjectId;

+ 48 - 0
src/server/models/openapi/paginate-result.js

@@ -0,0 +1,48 @@
+/**
+ * @see https://www.npmjs.com/package/mongoose-paginate-v2
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      PaginateResult:
+ *        type: object
+ *        properties:
+ *          docs:
+ *            type: array
+ *            description: Array of documents
+ *            items:
+ *              type: object
+ *          totalDocs:
+ *            type: number
+ *            description: Total number of documents in collection that match a query
+ *          limit:
+ *            type: number
+ *            description: Limit that was used
+ *          hasPrevPage:
+ *            type: number
+ *            description: Availability of prev page.
+ *          hasNextPage:
+ *            type: number
+ *            description: Availability of next page.
+ *          page:
+ *            type: number
+ *            description: Current page number
+ *          totalPages:
+ *            type: number
+ *            description: Total number of pages.
+ *          offset:
+ *            type: number
+ *            description: Only if specified or default page/offset values were used
+ *          prefPage:
+ *            type: number
+ *            description: Previous page number if available or NULL
+ *          nextPage:
+ *            type: number
+ *            description: Next page number if available or NULL
+ *          pagingCounter:
+ *            type: number
+ *            description: The starting sl. number of first document.
+ *          meta:
+ *            type: number
+ *            description: Object of pagination meta data (Default false).
+ */

+ 1 - 1
src/server/models/page-tag-relation.js

@@ -4,7 +4,7 @@
 const flatMap = require('array.prototype.flatmap');
 const flatMap = require('array.prototype.flatmap');
 
 
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
-const mongoosePaginate = require('mongoose-paginate');
+const mongoosePaginate = require('mongoose-paginate-v2');
 
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
 

+ 1 - 1
src/server/models/page.js

@@ -7,7 +7,7 @@ const debug = require('debug')('growi:models:page');
 const nodePath = require('path');
 const nodePath = require('path');
 const urljoin = require('url-join');
 const urljoin = require('url-join');
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
-const mongoosePaginate = require('mongoose-paginate');
+const mongoosePaginate = require('mongoose-paginate-v2');
 const uniqueValidator = require('mongoose-unique-validator');
 const uniqueValidator = require('mongoose-unique-validator');
 
 
 const { pathUtils } = require('growi-commons');
 const { pathUtils } = require('growi-commons');

+ 1 - 1
src/server/models/tag.js

@@ -2,7 +2,7 @@
 /* eslint-disable no-return-await */
 /* eslint-disable no-return-await */
 
 
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
-const mongoosePaginate = require('mongoose-paginate');
+const mongoosePaginate = require('mongoose-paginate-v2');
 
 
 /*
 /*
  * define schema
  * define schema

+ 1 - 1
src/server/models/user-group-relation.js

@@ -1,6 +1,6 @@
 const debug = require('debug')('growi:models:userGroupRelation');
 const debug = require('debug')('growi:models:userGroupRelation');
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
-const mongoosePaginate = require('mongoose-paginate');
+const mongoosePaginate = require('mongoose-paginate-v2');
 
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
 

+ 1 - 1
src/server/models/user-group.js

@@ -1,6 +1,6 @@
 const debug = require('debug')('growi:models:userGroup');
 const debug = require('debug')('growi:models:userGroup');
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
-const mongoosePaginate = require('mongoose-paginate');
+const mongoosePaginate = require('mongoose-paginate-v2');
 
 
 
 
 /*
 /*

+ 4 - 18
src/server/models/user.js

@@ -3,9 +3,9 @@
 const debug = require('debug')('growi:models:user');
 const debug = require('debug')('growi:models:user');
 const logger = require('@alias/logger')('growi:models:user');
 const logger = require('@alias/logger')('growi:models:user');
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
+const mongoosePaginate = require('mongoose-paginate-v2');
 const path = require('path');
 const path = require('path');
 const uniqueValidator = require('mongoose-unique-validator');
 const uniqueValidator = require('mongoose-unique-validator');
-const mongoosePaginate = require('mongoose-paginate');
 
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
 const ObjectId = mongoose.Schema.Types.ObjectId;
 const crypto = require('crypto');
 const crypto = require('crypto');
@@ -426,17 +426,6 @@ module.exports = function(crowi) {
       });
       });
   };
   };
 
 
-  userSchema.statics.findUsersWithPagination = async function(options) {
-    const defaultOptions = {
-      sort: { status: 1, username: 1, createdAt: 1 },
-      page: 1,
-      limit: PAGE_ITEMS,
-    };
-    const mergedOptions = Object.assign(defaultOptions, options);
-
-    return this.paginate({ status: { $ne: STATUS_DELETED } }, mergedOptions);
-  };
-
   userSchema.statics.findUsersByPartOfEmail = function(emailPart, options) {
   userSchema.statics.findUsersByPartOfEmail = function(emailPart, options) {
     const status = options.status || null;
     const status = options.status || null;
     const emailPartRegExp = new RegExp(emailPart.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'));
     const emailPartRegExp = new RegExp(emailPart.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'));
@@ -504,15 +493,12 @@ module.exports = function(crowi) {
   };
   };
 
 
   userSchema.statics.isUserCountExceedsUpperLimit = async function() {
   userSchema.statics.isUserCountExceedsUpperLimit = async function() {
-    const { aclService } = crowi;
+    const { configManager } = crowi;
 
 
-    const userUpperLimit = aclService.userUpperLimit();
-    if (userUpperLimit === 0) {
-      return false;
-    }
+    const userUpperLimit = configManager.getConfig('crowi', 'security:userUpperLimit');
 
 
     const activeUsers = await this.countListByStatus(STATUS_ACTIVE);
     const activeUsers = await this.countListByStatus(STATUS_ACTIVE);
-    if (userUpperLimit !== 0 && userUpperLimit <= activeUsers) {
+    if (userUpperLimit <= activeUsers) {
       return true;
       return true;
     }
     }
 
 

+ 1 - 21
src/server/routes/admin.js

@@ -445,27 +445,7 @@ module.exports = function(crowi, app) {
 
 
   actions.user = {};
   actions.user = {};
   actions.user.index = async function(req, res) {
   actions.user.index = async function(req, res) {
-    const activeUsers = await User.countListByStatus(User.STATUS_ACTIVE);
-    const userUpperLimit = aclService.userUpperLimit();
-    const isUserCountExceedsUpperLimit = await User.isUserCountExceedsUpperLimit();
-
-    const page = parseInt(req.query.page) || 1;
-
-    const result = await User.findUsersWithPagination({
-      page,
-      select: `${User.USER_PUBLIC_FIELDS} lastLoginAt`,
-      populate: User.IMAGE_POPULATION,
-    });
-
-    const pager = createPager(result.total, result.limit, result.page, result.pages, MAX_PAGE_LIST);
-
-    return res.render('admin/users', {
-      users: result.docs,
-      pager,
-      activeUsers,
-      userUpperLimit,
-      isUserCountExceedsUpperLimit,
-    });
+    return res.render('admin/users');
   };
   };
 
 
   // これやったときの relation の挙動未確認
   // これやったときの relation の挙動未確認

+ 16 - 9
src/server/routes/apiv3/users.js

@@ -9,6 +9,8 @@ const router = express.Router();
 const { body } = require('express-validator/check');
 const { body } = require('express-validator/check');
 const { isEmail } = require('validator');
 const { isEmail } = require('validator');
 
 
+const PAGE_ITEMS = 50;
+
 const validator = {};
 const validator = {};
 
 
 /**
 /**
@@ -17,7 +19,6 @@ const validator = {};
  *    name: Users
  *    name: Users
  */
  */
 
 
-
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const loginRequiredStrictly = require('../../middleware/login-required')(crowi);
   const loginRequiredStrictly = require('../../middleware/login-required')(crowi);
   const adminRequired = require('../../middleware/admin-required')(crowi);
   const adminRequired = require('../../middleware/admin-required')(crowi);
@@ -47,21 +48,27 @@ module.exports = (crowi) => {
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
    *                  properties:
    *                  properties:
-   *                    users:
-   *                      type: object
-   *                      description: a result of `Users.find`
+   *                    paginateResult:
+   *                      $ref: '#/components/schemas/PaginateResult'
    */
    */
   router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
   router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
+    const page = parseInt(req.query.page) || 1;
+
     try {
     try {
-      const page = parseInt(req.query.page) || 1;
-      const result = await User.findUsersWithPagination({ page });
-      const { docs: users, total: totalUsers, limit: pagingLimit } = result;
-      return res.apiv3({ users, totalUsers, pagingLimit });
+      const paginateResult = await User.paginate(
+        { status: { $ne: User.STATUS_DELETED } },
+        {
+          sort: { status: 1, username: 1, createdAt: 1 },
+          page,
+          limit: PAGE_ITEMS,
+        },
+      );
+      return res.apiv3({ paginateResult });
     }
     }
     catch (err) {
     catch (err) {
       const msg = 'Error occurred in fetching user group list';
       const msg = 'Error occurred in fetching user group list';
       logger.error('Error', err);
       logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'user-group-list-fetch-failed'));
+      return res.apiv3Err(new ErrorV3(msg, 'user-group-list-fetch-failed'), 500);
     }
     }
   });
   });
 
 

+ 0 - 11
src/server/service/acl.js

@@ -79,17 +79,6 @@ class AclService {
     return labels;
     return labels;
   }
   }
 
 
-  userUpperLimit() {
-    // const limit = this.configManager.getConfig('crowi', 'USER_UPPER_LIMIT');
-    const limit = process.env.USER_UPPER_LIMIT;
-
-    if (limit) {
-      return Number(limit);
-    }
-
-    return 0;
-  }
-
 }
 }
 
 
 module.exports = AclService;
 module.exports = AclService;

+ 10 - 1
src/server/service/config-loader.js

@@ -140,7 +140,10 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     ns:      'crowi',
     ns:      'crowi',
     key:     'gridfs:totalLimit',
     key:     'gridfs:totalLimit',
     type:    TYPES.NUMBER,
     type:    TYPES.NUMBER,
-    default: null,
+    default: null, // set null in default for backward compatibility
+    //                cz: Newer system respects FILE_UPLOAD_TOTAL_LIMIT.
+    //                    If the default value of MONGO_GRIDFS_TOTAL_LIMIT is Infinity,
+    //                      the system can't distinguish between "not specified" and "Infinity is specified".
   },
   },
   FORCE_WIKI_MODE: {
   FORCE_WIKI_MODE: {
     ns:      'crowi',
     ns:      'crowi',
@@ -148,6 +151,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.STRING,
     type:    TYPES.STRING,
     default: undefined,
     default: undefined,
   },
   },
+  USER_UPPER_LIMIT: {
+    ns:      'crowi',
+    key:     'security:userUpperLimit',
+    type:    TYPES.NUMBER,
+    default: Infinity,
+  },
   LOCAL_STRATEGY_ENABLED: {
   LOCAL_STRATEGY_ENABLED: {
     ns:      'crowi',
     ns:      'crowi',
     key:     'security:passport-local:isEnabled',
     key:     'security:passport-local:isEnabled',

+ 0 - 321
src/server/views/admin/Users_reserve.html

@@ -1,321 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitle(t('User_Management')) }}{% endblock %}
-
-{% block content_header %}
-<div class="header-wrap">
-  <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('User_Management') }}</h1>
-  </header>
-</div>
-{% endblock %}
-
-{% block content_main %}
-<div class="content-main">
-  {% set smessage = req.flash('successMessage') %}
-  {% if smessage.length %}
-  <div class="alert alert-success">
-    {{ smessage }}
-  </div>
-  {% endif %}
-
-  {% set emessage = req.flash('errorMessage') %}
-  {% if emessage.length %}
-  <div class="alert alert-danger">
-    {{ emessage }}
-  </div>
-  {% endif %}
-
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'user'} %}
-    </div>
-
-    <div class="col-md-9">
-      <p>
-        <button data-toggle="collapse" class="btn btn-default" href="#inviteUserForm" {% if isUserCountExceedsUpperLimit %}disabled{% endif %}>
-          {{ t("user_management.invite_users") }}
-        </button>
-        <a class="btn btn-default btn-outline" href="/admin/users/external-accounts">
-          <i class="icon-user-follow" aria-hidden="true"></i>
-          {{ t("user_management.external_account") }}
-        </a>
-      </p>
-      <form role="form" action="/admin/user/invite" method="post">
-        <div id="inviteUserForm" class="collapse">
-          <div class="form-group">
-            <label for="inviteForm[emailList]">{{ t('user_management.emails') }}</label>
-            <textarea class="form-control" name="inviteForm[emailList]" placeholder="{{ t('eg') }} user@growi.org"></textarea>
-          </div>
-          <div class="checkbox checkbox-info">
-            <input type="checkbox" id="inviteWithEmail" name="inviteForm[sendEmail]" checked>
-            <label for="inviteWithEmail">{{ t('user_management.invite_thru_email') }}</label>
-          </div>
-          <button type="submit" class="btn btn-primary">{{ t('user_management.invite') }}</button>
-        </div>
-        <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      </form>
-
-      {% if isUserCountExceedsUpperLimit === true %}
-      <label>{{ t('user_management.cannot_invite_maximum_users') }}</label>
-      {% endif %}
-      {% if userUpperLimit !== 0 %}
-      <label>{{ t('user_management.current_users') }}{{ activeUsers }}</label>
-      {% endif %}
-
-      {% 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>
-              <div class="modal-title">{{ t('user_management.invited') }}</div>
-            </div>
-
-            <div class="modal-body">
-              <p>
-                {{ t('user_management.temporary_password') }}<br>
-                {{ t('user_management.password_never_seen') }}<span class="text-danger">{{ t('user_management.send_temporary_password') }}</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 -->
-      {% 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>
-              <div class="modal-title">{{ t('user_management.reset_password')}}</div>
-            </div>
-
-            <div class="modal-body">
-              <p>
-                {{ t('user_management.password_never_seen') }}<br>
-              <span class="text-danger">{{ t('user_management.send_new_password') }}</span>
-              </p>
-              <p>
-              {{ t('user_management.target_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">
-                  {{ t('user_management.reset_password')}}
-                </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>
-              <div class="modal-title">{{ t('user_management.reset_password') }}</div>
-            </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 password: <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>{{ t("User_Management") }}</h2>
-
-      <table class="table table-default table-bordered table-user-list">
-        <thead>
-          <tr>
-            <th width="100px">#</th>
-            <th>{{ t('status') }}</th>
-            <th><code>{{ t('User') }}</code></th>
-            <th>{{ t('Name') }}</th>
-            <th>{{ t('Email') }}</th>
-            <th width="100px">{{ t('Created') }}</th>
-            <th width="150px">{{ t('Last_Login') }}</th>
-            <th width="70px"></th>
-          </tr>
-        </thead>
-        <tbody>
-          {% for sUser in users %}
-          {% set sUserId = sUser._id.toString() %}
-          <tr>
-            <td>
-              <img src="{{ sUser|picture }}" class="picture img-circle" />
-              {% if sUser.admin %}
-              <span class="label label-inverse label-admin">
-              {{ t('administrator') }}
-              </span>
-              {% endif %}
-            </td>
-            <td>
-              <span class="label {{ css.userStatus(sUser) }}">
-                {{ consts.userStatus[sUser.status] }}
-              </span>
-            </td>
-            <td>
-              <strong>{{ sUser.username }}</strong>
-            </td>
-            <td>{{ sUser.name }}</td>
-            <td>{{ sUser.email }}</td>
-            <td>{{ sUser.createdAt|date('Y-m-d', sUser.createdAt.getTimezoneOffset()) }}</td>
-            <td>
-              {% if sUser.lastLoginAt %}
-                {{ sUser.lastLoginAt|date('Y-m-d H:i', sUser.createdAt.getTimezoneOffset()) }}
-              {% endif %}
-            </td>
-            <td>
-              <div class="btn-group admin-user-menu">
-                <button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown">
-                  <i class="icon-settings"></i> <span class="caret"></span>
-                </button>
-                <ul class="dropdown-menu" role="menu">
-                  <li class="dropdown-header">{{ t('user_management.edit_menu') }}</li>
-                  <li>
-                    <a href="#"
-                        data-user-id="{{ sUserId }}"
-                        data-user-email="{{ sUser.email }}"
-                        data-target="#admin-password-reset-modal"
-                        data-toggle="modal">
-                      <i class="icon-fw icon-key"></i>
-                      {{ t('user_management.reset_password') }}
-                    </a>
-                  </li>
-                  <li class="divider"></li>
-                  <li class="dropdown-header">{{ t('status') }}</li>
-
-                  {% if sUser.status == 1 %}
-                  <form id="form_activate_{{ sUserId }}" action="/admin/user/{{ sUserId }}/activate" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    <a href="javascript:form_activate_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-user-following"></i> {{ t('user_management.accept') }}
-                    </a>
-                  </li>
-                  {% endif  %}
-
-                  {% if sUser.status == 2 %}
-                  <form id="form_suspend_{{ sUserId }}" action="/admin/user/{{ sUserId }}/suspend" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    {% if sUser.username != user.username %}
-                    <a href="javascript:form_suspend_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-ban"></i>
-                      {{ t('user_management.deactivate_account') }}
-                    </a>
-                    {% else %}
-                    <a disabled>
-                      <i class="icon-fw icon-ban"></i>
-                      {{ t('user_management.deactivate_account') }}
-                    </a>
-                    <p class="alert alert-danger m-l-10 m-r-10 p-10">{{ t("user_management.your_own") }}</p>
-                    {% endif %}
-                  </li>
-                  {% endif %}
-
-                  {% if sUser.status == 3 %}
-                  <form id="form_activate_{{ sUserId }}" action="/admin/user/{{ sUserId }}/activate" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <form id="form_remove_{{ sUserId }}" action="/admin/user/{{ sUserId }}/remove" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    <a href="javascript:form_activate_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-action-redo"></i> {{ t('Undo') }}
-                    </a>
-                  </li>
-                  <li>
-                    {# label は同じだけど、こっちは論理削除 #}
-                    <a href="javascript:form_remove_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}
-                    </a>
-                  </li>
-                  {% endif %}
-
-                  {% if sUser.status == 1 || sUser.status == 5 %}
-                  <form id="form_removeCompletely_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/removeCompletely" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li class="dropdown-button">
-                    {# label は同じだけど、こっちは物理削除 #}
-                    <a href="javascript:form_removeCompletely_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}
-                    </a>
-                  </li>
-                  {% endif %}
-
-                  {% if sUser.status == 2 %} {# activated な人だけこのメニューを表示 #}
-                  <li class="divider"></li>
-                  <li class="dropdown-header">{{ t('user_management.administrator_menu') }}</li>
-
-                  {% if sUser.admin %}
-                  <form id="form_removeFromAdmin_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/removeFromAdmin" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    {% if sUser.username != user.username %}
-                      <a href="javascript:form_removeFromAdmin_{{ sUserId }}.submit()">
-                        <i class="icon-fw icon-user-unfollow"></i> {{ t("user_management.remove_admin_access") }}
-                      </a>
-                    {% else %}
-                      <a disabled>
-                        <i class="icon-fw icon-user-unfollow"></i> {{ t("user_management.remove_admin_access") }}
-                      </a>
-                      <p class="alert alert-danger m-l-10 m-r-10 p-10">{{ t("user_management.cannot_remove") }}</p>
-                    {% endif %}
-                  </li>
-                  {% else %}
-                  <form id="form_makeAdmin_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/makeAdmin" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    <a href="javascript:form_makeAdmin_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-magic-wand"></i> {{ t("user_management.give_admin_access") }}
-                    </a>
-                  </li>
-                  {% endif %}
-
-                  {% endif %}
-                </ul>
-              </div>
-            </td>
-          </tr>
-          {% endfor %}
-        </tbody>
-      </table>
-
-      {% include '../widget/pager.html' with {path: "/admin/users", pager: pager} %}
-
-    </div>
-  </div>
-</div>
-{% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %} -->

+ 2 - 3
src/server/views/admin/users.html

@@ -30,9 +30,8 @@
     {% include './widget/menu.html' with {current: 'user'} %}
     {% include './widget/menu.html' with {current: 'user'} %}
   </div>
   </div>
   <div
   <div
-  class="col-md-9"
-  id ="admin-user-page"
-  users= "{{ users | json }}"
+    class="col-md-9"
+    id ="admin-user-page"
   >
   >
   </div>
   </div>
 </div>
 </div>

+ 4 - 9
yarn.lock

@@ -2120,10 +2120,6 @@ block-stream@*:
   dependencies:
   dependencies:
     inherits "~2.0.0"
     inherits "~2.0.0"
 
 
-bluebird@3.0.5:
-  version "3.0.5"
-  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.0.5.tgz#2ff9d07c9b3edb29d6d280fe07528365e7ecd392"
-
 bluebird@3.5.1, bluebird@^3.5.1:
 bluebird@3.5.1, bluebird@^3.5.1:
   version "3.5.1"
   version "3.5.1"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
@@ -8424,11 +8420,10 @@ mongoose-legacy-pluralize@1.0.2:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4"
   resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4"
 
 
-mongoose-paginate@^5.0.3:
-  version "5.0.3"
-  resolved "https://registry.yarnpkg.com/mongoose-paginate/-/mongoose-paginate-5.0.3.tgz#d7ae49ed5bf64f1f7af7620ea865b67058c55371"
-  dependencies:
-    bluebird "3.0.5"
+mongoose-paginate-v2@^1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/mongoose-paginate-v2/-/mongoose-paginate-v2-1.3.2.tgz#4a6077255156c555879c857eb0350b16272ed113"
+  integrity sha512-z8fmLaUjJ8u6Q/zxd/6JEbwKB+MY7lp2NahWlFdPYqiVHGVuL2cOpW99t4JA+EgW59V2zxwv8ZSoN0mFDaVrqw==
 
 
 mongoose-schema-jsonschema@>=1.2.1:
 mongoose-schema-jsonschema@>=1.2.1:
   version "1.2.1"
   version "1.2.1"