diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index eab9d96c..22ed520a 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -69,6 +69,18 @@ Actions: info arguments: --security Include security information in output +list arguments: + --sort SORT_BY Specify field to sort by + + Valid SORT_BY values: + id : User ID + username : Username + realname : Real name + status : Account status + created : Account creation date + lastlogin : Last login timestamp + logins : Login count + 2fa-otp arguments: --qr-type TYPE Specify QR code type diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 255c7792..e5705ce5 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -17,6 +17,7 @@ const async = require('async'); const _ = require('lodash'); const moment = require('moment'); const fs = require('fs-extra'); +const Table = require('easy-table'); exports.handleUserCommand = handleUserCommand; @@ -340,7 +341,7 @@ function showUserInfo(user) { const statusDesc = () => { const status = user.properties[UserProps.AccountStatus]; - return _.invert(User.AccountStatus)[status] || 'unknown'; + return _.invert(User.AccountStatus)[status] || 'N/A'; }; const created = () => { @@ -508,7 +509,6 @@ function listUsers() { // :TODO: --created-since SPEC and --last-called SPEC // --created-since SPEC // SPEC can be TIMESTAMP or e.g. "-1hour" or "-90days" - // :TODO: --sort name|id let listWhat; if (argv._.length > 2) { listWhat = argv._[argv._.length - 1]; @@ -516,6 +516,8 @@ function listUsers() { listWhat = 'all'; } + const sortBy = (argv.sort || 'id').toLowerCase(); + const User = require('../../core/user'); if (!['all'].concat(Object.keys(User.AccountStatus)).includes(listWhat)) { return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); @@ -527,7 +529,13 @@ function listUsers() { const UserProps = require('../../core/user_property'); const userListOpts = { - properties: [UserProps.AccountStatus], + properties: [ + UserProps.RealName, + UserProps.AccountStatus, + UserProps.AccountCreated, + UserProps.LastLoginTs, + UserProps.LoginCount, + ], }; User.getUserList(userListOpts, (err, userList) => { @@ -550,10 +558,94 @@ function listUsers() { }); }, (userList, callback) => { + // default sort: by ID + const sortById = (left, right) => { + return left.userId - right.userId; + }; + + const sortByLogin = prop => (left, right) => { + return parseInt(right[prop]) - parseInt(left[prop]); + }; + + const sortByString = prop => (left, right) => { + return left[prop].localeCompare(right[prop], { + sensitivity: false, + numeric: true, + }); + }; + + const sortByTimestamp = prop => (left, right) => { + return moment(left[prop]) - moment(right[prop]); + }; + + let sorter; + switch (sortBy) { + case 'username': + sorter = sortByString('userName'); + break; + case 'realname': + sorter = sortByString(UserProps.RealName); + break; + case 'status': + sorter = sortByString(UserProps.AccountStatus); + break; + case 'created': + sorter = sortByTimestamp(UserProps.AccountCreated); + break; + case 'lastlogin': + sorter = sortByTimestamp(UserProps.LastLoginTs); + break; + case 'logins': + sorter = sortByLogin(UserProps.LoginCount); + break; + + case 'id': + default: + sorter = sortById; + break; + } + + userList.sort(sorter); + + const StatusNames = _.invert(User.AccountStatus); + + const propOrNA = (user, prop) => { + return user[prop] || 'N/A'; + }; + + const timestampOrNA = (user, prop, format) => { + let ts = user[prop]; + return ts ? moment(ts).format(format) : 'N/A'; + }; + + const makeAccountStatus = status => { + return StatusNames[status] || 'N/A'; + }; + + const table = new Table(); userList.forEach(user => { - console.info(`${user.userId}: ${user.userName}`); + table.cell('ID', user.userId); + table.cell('Username', user.userName); + table.cell('Real Name', user[UserProps.RealName]); + table.cell( + 'Status', + makeAccountStatus(user[UserProps.AccountStatus]) + ); + table.cell( + 'Created', + timestampOrNA(user, UserProps.AccountCreated, 'YYYY-MM-DD') + ); + table.cell( + 'Last Login', + timestampOrNA(user, UserProps.LastLoginTs, 'YYYY-MM-DD HH::mm') + ); + table.cell('Logins', propOrNA(user, UserProps.LoginCount)); + + table.newRow(); }); + console.info(table.toString()); + return callback(null); }, ], diff --git a/package.json b/package.json index 9d0e6d8e..4a088ee9 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "uuid": "8.3.2", "uuid-parse": "1.1.0", "ws": "7.4.3", - "yazl": "^2.5.1" + "yazl": "^2.5.1", + "easy-table": "^1.2.0" }, "devDependencies": { "eslint": "8.21.0", diff --git a/yarn.lock b/yarn.lock index c406871d..7a6f48f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -602,6 +602,15 @@ dtrace-provider@~0.8: dependencies: nan "^2.10.0" +easy-table@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/easy-table/-/easy-table-1.2.0.tgz#ba9225d7138fee307bfd4f0b5bc3c04bdc7c54eb" + integrity sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww== + dependencies: + ansi-regex "^5.0.1" + optionalDependencies: + wcwidth "^1.0.1" + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"