diff --git a/public/language/en-GB/admin/manage/users.json b/public/language/en-GB/admin/manage/users.json index 2712dece9d..d0d95b380f 100644 --- a/public/language/en-GB/admin/manage/users.json +++ b/public/language/en-GB/admin/manage/users.json @@ -20,16 +20,10 @@ "add-group": "Add Group", "invite": "Invite", "new": "New User", - - "pills.latest": "Latest Users", + "filter-by": "Filter by", "pills.unvalidated": "Not Validated", - "pills.no-posts": "No Posts", - "pills.top-posters": "Top Posters", - "pills.top-rep": "Most Reputation", - "pills.inactive": "Inactive", - "pills.flagged": "Most Flagged", + "pills.validated": "Validated", "pills.banned": "Banned", - "pills.search": "User Search", "50-per-page": "50 per page", "100-per-page": "100 per page", diff --git a/public/openapi/read.yaml b/public/openapi/read.yaml index 02dad71349..885307aee8 100644 --- a/public/openapi/read.yaml +++ b/public/openapi/read.yaml @@ -79,24 +79,6 @@ paths: $ref: 'read/admin/manage/tags.yaml' /api/admin/manage/users: $ref: 'read/admin/manage/users.yaml' - /api/admin/manage/users/search: - $ref: 'read/admin/manage/users/search.yaml' - /api/admin/manage/users/latest: - $ref: 'read/admin/manage/users/latest.yaml' - /api/admin/manage/users/not-validated: - $ref: 'read/admin/manage/users/not-validated.yaml' - /api/admin/manage/users/no-posts: - $ref: 'read/admin/manage/users/no-posts.yaml' - /api/admin/manage/users/top-posters: - $ref: 'read/admin/manage/users/top-posters.yaml' - /api/admin/manage/users/most-reputation: - $ref: 'read/admin/manage/users/most-reputation.yaml' - /api/admin/manage/users/inactive: - $ref: 'read/admin/manage/users/inactive.yaml' - /api/admin/manage/users/flagged: - $ref: 'read/admin/manage/users/flagged.yaml' - /api/admin/manage/users/banned: - $ref: 'read/admin/manage/users/banned.yaml' /api/admin/manage/registration: $ref: 'read/admin/manage/registration.yaml' /api/admin/manage/admins-mods: diff --git a/public/openapi/read/admin/manage/privileges/cid.yaml b/public/openapi/read/admin/manage/privileges/cid.yaml index 07f6de868a..ddeadfefd3 100644 --- a/public/openapi/read/admin/manage/privileges/cid.yaml +++ b/public/openapi/read/admin/manage/privileges/cid.yaml @@ -79,6 +79,8 @@ get: description: Each privilege will have a key in this object isPrivate: type: boolean + isSystem: + type: boolean columnCountUser: type: number columnCountUserOther: diff --git a/public/openapi/read/admin/manage/users.yaml b/public/openapi/read/admin/manage/users.yaml index 5248bb4d52..8bca9c7661 100644 --- a/public/openapi/read/admin/manage/users.yaml +++ b/public/openapi/read/admin/manage/users.yaml @@ -21,12 +21,12 @@ get: type: number resultsPerPage: type: number - latest: + reverse: type: boolean - search_display: + sortBy: type: string - requireEmailConfirmation: - type: number + sort_online: + type: boolean inviteOnly: type: boolean adminInviteOnly: diff --git a/public/openapi/read/admin/manage/users/banned.yaml b/public/openapi/read/admin/manage/users/banned.yaml deleted file mode 100644 index 8c0d42a954..0000000000 --- a/public/openapi/read/admin/manage/users/banned.yaml +++ /dev/null @@ -1,7 +0,0 @@ -get: - tags: - - admin - summary: Get banned users - responses: - "418": - description: "TODO: A proper response needs to be added. It is not really a teapot | Replace this responses block with the block from /manage/users/latest" \ No newline at end of file diff --git a/public/openapi/read/admin/manage/users/flagged.yaml b/public/openapi/read/admin/manage/users/flagged.yaml deleted file mode 100644 index 058e9f480e..0000000000 --- a/public/openapi/read/admin/manage/users/flagged.yaml +++ /dev/null @@ -1,7 +0,0 @@ -get: - tags: - - admin - summary: Get flagged users - responses: - "418": - description: "TODO: A proper response needs to be added. It is not really a teapot | Replace this responses block with the block from /manage/users/latest" \ No newline at end of file diff --git a/public/openapi/read/admin/manage/users/inactive.yaml b/public/openapi/read/admin/manage/users/inactive.yaml deleted file mode 100644 index 60c01f8eb9..0000000000 --- a/public/openapi/read/admin/manage/users/inactive.yaml +++ /dev/null @@ -1,7 +0,0 @@ -get: - tags: - - admin - summary: Get inactive users - responses: - "418": - description: "TODO: A proper response needs to be added. It is not really a teapot | Replace this responses block with the block from /manage/users/latest" \ No newline at end of file diff --git a/public/openapi/read/admin/manage/users/latest.yaml b/public/openapi/read/admin/manage/users/latest.yaml deleted file mode 100644 index b965d0c4c2..0000000000 --- a/public/openapi/read/admin/manage/users/latest.yaml +++ /dev/null @@ -1,7 +0,0 @@ -get: - tags: - - admin - summary: Get latest users - responses: - "418": - description: "TODO: A proper response needs to be added. It is not really a teapot | Replace this responses block with the block from /manage/users/latest" \ No newline at end of file diff --git a/public/openapi/read/admin/manage/users/most-reputation.yaml b/public/openapi/read/admin/manage/users/most-reputation.yaml deleted file mode 100644 index 81758e8d60..0000000000 --- a/public/openapi/read/admin/manage/users/most-reputation.yaml +++ /dev/null @@ -1,7 +0,0 @@ -get: - tags: - - admin - summary: Get users with the most reputation - responses: - "418": - description: "TODO: A proper response needs to be added. It is not really a teapot | Replace this responses block with the block from /manage/users/latest" \ No newline at end of file diff --git a/public/openapi/read/admin/manage/users/no-posts.yaml b/public/openapi/read/admin/manage/users/no-posts.yaml deleted file mode 100644 index 4d0e85b337..0000000000 --- a/public/openapi/read/admin/manage/users/no-posts.yaml +++ /dev/null @@ -1,7 +0,0 @@ -get: - tags: - - admin - summary: Get users with no posts - responses: - "418": - description: "TODO: A proper response needs to be added. It is not really a teapot | Replace this responses block with the block from /manage/users/latest" \ No newline at end of file diff --git a/public/openapi/read/admin/manage/users/not-validated.yaml b/public/openapi/read/admin/manage/users/not-validated.yaml deleted file mode 100644 index 2a2e6fd29a..0000000000 --- a/public/openapi/read/admin/manage/users/not-validated.yaml +++ /dev/null @@ -1,7 +0,0 @@ -get: - tags: - - admin - summary: Get non-verified users - responses: - "418": - description: "TODO: A proper response needs to be added. It is not really a teapot | Replace this responses block with the block from /manage/users/latest" \ No newline at end of file diff --git a/public/openapi/read/admin/manage/users/search.yaml b/public/openapi/read/admin/manage/users/search.yaml deleted file mode 100644 index bb5c8d7eaa..0000000000 --- a/public/openapi/read/admin/manage/users/search.yaml +++ /dev/null @@ -1,39 +0,0 @@ -get: - tags: - - admin - summary: Get users via search term - responses: - "200": - description: "" - content: - application/json: - schema: - allOf: - - type: object - properties: - search_display: - type: string - matchCount: - type: number - query: - type: string - uidQuery: - type: string - usernameQuery: - type: string - emailQuery: - type: string - ipQuery: - type: string - pageCount: - type: number - resultsPerPage: - type: number - timing: - type: number - users: - type: array - items: - $ref: ../../../../components/schemas/UserObject.yaml#/UserObjectACP - - $ref: ../../../../components/schemas/CommonProps.yaml#/CommonProps - - $ref: ../../../../components/schemas/Pagination.yaml#/Pagination \ No newline at end of file diff --git a/public/openapi/read/admin/manage/users/top-posters.yaml b/public/openapi/read/admin/manage/users/top-posters.yaml deleted file mode 100644 index 41c11599d5..0000000000 --- a/public/openapi/read/admin/manage/users/top-posters.yaml +++ /dev/null @@ -1,7 +0,0 @@ -get: - tags: - - admin - summary: Get users with the most posts - responses: - "418": - description: "TODO: A proper response needs to be added. It is not really a teapot | Replace this responses block with the block from /manage/users/latest" \ No newline at end of file diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index 7b71139304..4317a40753 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -6,17 +6,11 @@ define('admin/manage/users', [ var Users = {}; Users.init = function () { - var navPills = $('.nav-pills li'); - var pathname = window.location.pathname; - if (!navPills.find('a[href^="' + pathname + '"]').length || pathname === config.relative_path + '/admin/manage/users') { - pathname = config.relative_path + '/admin/manage/users/latest'; - } - navPills.removeClass('active').find('a[href^="' + pathname + '"]').parent().addClass('active'); - $('#results-per-page').val(ajaxify.data.resultsPerPage).on('change', function () { var query = utils.params(); query.resultsPerPage = $('#results-per-page').val(); - ajaxify.go(window.location.pathname + '?' + $.param(query)); + var qs = buildSearchQuery(query); + ajaxify.go(window.location.pathname + '?' + qs); }); function getSelectedUids() { @@ -346,7 +340,7 @@ define('admin/manage/users', [ }); function handleUserCreate() { - $('#createUser').on('click', function () { + $('[data-action="create"]').on('click', function () { Benchpress.parse('admin/partials/create_user_modal', {}, function (html) { var modal = bootbox.dialog({ message: html, @@ -403,35 +397,45 @@ define('admin/manage/users', [ }, err => errorEl.translateHtml('[[admin/manage/users:alerts.error-x, ' + err.status.message + ']]').removeClass('hidden')); } - var timeoutId = 0; + handleSearch(); + + handleUserCreate(); + + handleInvite(); - $('#search-user-uid, #search-user-name, #search-user-email, #search-user-ip').on('keyup', function () { + handleSort(); + handleFilter(); + }; + + function handleSearch() { + var timeoutId = 0; + function doSearch() { + $('.fa-spinner').removeClass('hidden'); + loadSearchPage({ + searchBy: $('#user-search-by').val(), + query: $('#user-search').val(), + page: 1, + }); + } + $('#user-search').on('keyup', function () { if (timeoutId !== 0) { clearTimeout(timeoutId); timeoutId = 0; } - - var $this = $(this); - var type = $this.attr('data-search-type'); - - timeoutId = setTimeout(function () { - $('.fa-spinner').removeClass('hidden'); - loadSearchPage({ - searchBy: type, - query: $this.val(), - page: 1, - }); - }, 250); + timeoutId = setTimeout(doSearch, 250); }); - - handleUserCreate(); - - handleInvite(); - }; + $('#user-search-by').on('change', function () { + doSearch(); + }); + } function loadSearchPage(query) { - var qs = decodeURIComponent($.param(query)); - $.get(config.relative_path + '/api/admin/manage/users/search?' + qs, renderSearchResults).fail(function (xhrErr) { + var params = utils.params(); + params.searchBy = query.searchBy; + params.query = query.query; + params.page = query.page; + var qs = decodeURIComponent($.param(params)); + $.get(config.relative_path + '/api/admin/manage/users?' + qs, renderSearchResults).fail(function (xhrErr) { if (xhrErr && xhrErr.responseJSON && xhrErr.responseJSON.error) { app.alertError(xhrErr.responseJSON.error); } @@ -448,14 +452,19 @@ define('admin/manage/users', [ $('.users-table tbody').append(html); html.find('.timeago').timeago(); $('.fa-spinner').addClass('hidden'); - + if (!$('#user-search').val()) { + $('#user-found-notify').addClass('hidden'); + $('#user-notfound-notify').addClass('hidden'); + return; + } if (data && data.users.length === 0) { $('#user-notfound-notify').translateHtml('[[admin/manage/users:search.not-found]]') .removeClass('hidden'); $('#user-found-notify').addClass('hidden'); } else { - $('#user-found-notify').translateHtml(translator.compile('admin/manage/users:alerts.x-users-found', data.matchCount, data.timing)) - .removeClass('hidden'); + $('#user-found-notify').translateHtml( + translator.compile('admin/manage/users:alerts.x-users-found', data.matchCount, data.timing) + ).removeClass('hidden'); $('#user-notfound-notify').addClass('hidden'); } }); @@ -479,6 +488,75 @@ define('admin/manage/users', [ }); } + function buildSearchQuery(params) { + if ($('#user-search').val()) { + params.query = $('#user-search').val(); + params.searchBy = $('#user-search-by').val(); + } else { + delete params.query; + delete params.searchBy; + } + + return decodeURIComponent($.param(params)); + } + + function handleSort() { + $('.users-table thead th').on('click', function () { + var $this = $(this); + var sortBy = $this.attr('data-sort'); + if (!sortBy) { + return; + } + var params = utils.params(); + params.sortBy = sortBy; + if (ajaxify.data.sortBy === sortBy) { + params.sortDirection = ajaxify.data.reverse ? 'asc' : 'desc'; + } else { + params.sortDirection = 'desc'; + } + + var qs = buildSearchQuery(params); + ajaxify.go('admin/manage/users?' + qs); + }); + } + + function getFilters() { + var filters = []; + $('#filter-by').find('[data-filter-by]').each(function () { + if ($(this).find('.fa-check').length) { + filters.push($(this).attr('data-filter-by')); + } + }); + return filters; + } + + function handleFilter() { + var currentFilters = getFilters(); + $('#filter-by').on('click', 'li', function () { + var $this = $(this); + $this.find('i').toggleClass('fa-check', !$this.find('i').hasClass('fa-check')); + return false; + }); + + $('#filter-by').on('hidden.bs.dropdown', function () { + var filters = getFilters(); + var changed = filters.length !== currentFilters.length; + if (filters.length === currentFilters.length) { + filters.forEach(function (filter, i) { + if (filter !== currentFilters[i]) { + changed = true; + } + }); + } + currentFilters = getFilters(); + if (changed) { + var params = utils.params(); + params.filters = filters; + var qs = buildSearchQuery(params); + ajaxify.go('admin/manage/users?' + qs); + } + }); + } return Users; }); diff --git a/public/src/client/topic/change-owner.js b/public/src/client/topic/change-owner.js index 771b3b6345..04fda698f5 100644 --- a/public/src/client/topic/change-owner.js +++ b/public/src/client/topic/change-owner.js @@ -37,7 +37,7 @@ define('forum/topic/change-owner', [ changeOwner(); }); - autocomplete.user(modal.find('#username'), { notBanned: true }, function (ev, ui) { + autocomplete.user(modal.find('#username'), { filters: ['notbanned'] }, function (ev, ui) { toUid = ui.item.user.uid; checkButtonEnable(); }); diff --git a/public/src/client/users.js b/public/src/client/users.js index 732208cd60..3ff1a62c68 100644 --- a/public/src/client/users.js +++ b/public/src/client/users.js @@ -61,17 +61,20 @@ define('forum/users', ['translator', 'benchpress'], function (translator, Benchp return loadPage(query); } - query.term = username; + query.query = username; query.sortBy = getSortBy(); - + var filters = []; if ($('.search .online-only').is(':checked') || (activeSection === 'online')) { - query.onlineOnly = true; + filters.push('online'); } if (activeSection === 'banned') { - query.bannedOnly = true; + filters.push('banned'); } if (activeSection === 'flagged') { - query.flaggedOnly = true; + filters.push('flagged'); + } + if (filters.length) { + query.filters = filters; } loadPage(query); diff --git a/src/controllers/admin/groups.js b/src/controllers/admin/groups.js index 51614c7a7f..c099d2093e 100644 --- a/src/controllers/admin/groups.js +++ b/src/controllers/admin/groups.js @@ -67,7 +67,11 @@ groupsController.get = async function (req, res, next) { async function getGroupNames() { const groupNames = await db.getSortedSetRange('groups:createtime', 0, -1); - return groupNames.filter(name => name !== 'registered-users' && !groups.isPrivilegeGroup(name)); + return groupNames.filter(name => name !== 'registered-users' && + name !== 'verified-users' && + name !== 'unverified-users' && + !groups.isPrivilegeGroup(name) + ); } groupsController.getCSV = async function (req, res) { diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js index ef8a9fdb64..b505fda6d2 100644 --- a/src/controllers/admin/users.js +++ b/src/controllers/admin/users.js @@ -13,19 +13,130 @@ const utils = require('../../utils'); const usersController = module.exports; -const userFields = ['uid', 'username', 'userslug', 'email', 'postcount', 'joindate', 'banned', - 'reputation', 'picture', 'flags', 'lastonline', 'email:confirmed']; +const userFields = [ + 'uid', 'username', 'userslug', 'email', 'postcount', 'joindate', 'banned', + 'reputation', 'picture', 'flags', 'lastonline', 'email:confirmed', +]; + +usersController.index = async function (req, res) { + if (req.query.query) { + await usersController.search(req, res); + } else { + await getUsers(req, res); + } +}; + +async function getUsers(req, res) { + const sortDirection = req.query.sortDirection || 'desc'; + const reverse = sortDirection === 'desc'; + + const page = parseInt(req.query.page, 10) || 1; + let resultsPerPage = parseInt(req.query.resultsPerPage, 10) || 50; + if (![50, 100, 250, 500].includes(resultsPerPage)) { + resultsPerPage = 50; + } + let sortBy = validator.escape(req.query.sortBy || ''); + const filterBy = Array.isArray(req.query.filters || []) ? (req.query.filters || []) : [req.query.filters]; + const start = Math.max(0, page - 1) * resultsPerPage; + const stop = start + resultsPerPage - 1; + + function buildSet() { + const sortToSet = { + postcount: 'users:postcount', + reputation: 'users:reputation', + joindate: 'users:joindate', + online: 'users:online', + flags: 'users:flags', + }; + + const set = []; + if (sortBy) { + set.push(sortToSet[sortBy]); + } + if (filterBy.includes('unverified')) { + set.push('group:unverified-users:members'); + } + if (filterBy.includes('verified')) { + set.push('group:verified-users:members'); + } + if (filterBy.includes('banned')) { + set.push('users:banned'); + } + if (!set.length) { + set.push('users:online'); + sortBy = 'online'; + } + return set.length > 1 ? set : set[0]; + } + + async function getCount(set) { + if (Array.isArray(set)) { + return await db.sortedSetIntersectCard(set); + } + return await db.sortedSetCard(set); + } + + async function getUids(set) { + let uids = []; + if (Array.isArray(set)) { + const weights = set.map((s, index) => (index ? 0 : 1)); + uids = await db[reverse ? 'getSortedSetRevIntersect' : 'getSortedSetIntersect']({ + sets: set, + start: start, + stop: stop, + weights: weights, + }); + } else { + uids = await db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop); + } + return uids; + } + + async function getUsersWithFields(set) { + const uids = await getUids(set); + const [isAdmin, userData] = await Promise.all([ + user.isAdministrator(uids), + user.getUsersWithFields(uids, userFields, req.uid), + ]); + userData.forEach((user, index) => { + if (user) { + user.administrator = isAdmin[index]; + } + }); + return userData; + } + const set = buildSet(); + const [count, users] = await Promise.all([ + getCount(set), + getUsersWithFields(set), + ]); + + render(req, res, { + users: users.filter(user => user && parseInt(user.uid, 10)), + page: page, + pageCount: Math.max(1, Math.ceil(count / resultsPerPage)), + resultsPerPage: resultsPerPage, + reverse: reverse, + sortBy: sortBy, + }); +} usersController.search = async function (req, res) { + const sortDirection = req.query.sortDirection || 'desc'; + const reverse = sortDirection === 'desc'; const page = parseInt(req.query.page, 10) || 1; let resultsPerPage = parseInt(req.query.resultsPerPage, 10) || 50; if (![50, 100, 250, 500].includes(resultsPerPage)) { resultsPerPage = 50; } + const searchData = await user.search({ uid: req.uid, query: req.query.query, searchBy: req.query.searchBy, + sortBy: req.query.sortBy, + sortDirection: sortDirection, + filters: req.query.filters, page: page, resultsPerPage: resultsPerPage, findUids: async function (query, searchBy, hardCap) { @@ -58,48 +169,10 @@ usersController.search = async function (req, res) { } }); searchData.query = validator.escape(String(req.query.query || '')); - searchData.uidQuery = req.query.searchBy === 'uid' ? searchData.query : ''; - searchData.usernameQuery = req.query.searchBy === 'username' ? searchData.query : ''; - searchData.emailQuery = req.query.searchBy === 'email' ? searchData.query : ''; - searchData.ipQuery = req.query.searchBy === 'uid' ? searchData.query : ''; searchData.resultsPerPage = resultsPerPage; - searchData.pagination = pagination.create(page, searchData.pageCount, req.query); - searchData.search_display = ''; - res.render('admin/manage/users', searchData); -}; - -usersController.sortByJoinDate = async function (req, res) { - await getUsers('users:joindate', 'latest', undefined, undefined, req, res); -}; - -usersController.notValidated = async function (req, res) { - await getUsers('users:notvalidated', 'notvalidated', undefined, undefined, req, res); -}; - -usersController.noPosts = async function (req, res) { - await getUsers('users:postcount', 'noposts', '-inf', 0, req, res); -}; - -usersController.topPosters = async function (req, res) { - await getUsers('users:postcount', 'topposts', 0, '+inf', req, res); -}; - -usersController.mostReputaion = async function (req, res) { - await getUsers('users:reputation', 'mostreputation', 0, '+inf', req, res); -}; - -usersController.flagged = async function (req, res) { - await getUsers('users:flags', 'mostflags', 1, '+inf', req, res); -}; - -usersController.inactive = async function (req, res) { - const timeRange = 1000 * 60 * 60 * 24 * 30 * (parseInt(req.query.months, 10) || 3); - const cutoff = Date.now() - timeRange; - await getUsers('users:online', 'inactive', '-inf', cutoff, req, res); -}; - -usersController.banned = async function (req, res) { - await getUsers('users:banned', 'banned', undefined, undefined, req, res); + searchData.sortBy = req.query.sortBy; + searchData.reverse = reverse; + render(req, res, searchData); }; usersController.registrationQueue = async function (req, res) { @@ -149,69 +222,21 @@ async function getInvites() { return invitations; } -async function getUsers(set, section, min, max, req, res) { - const page = parseInt(req.query.page, 10) || 1; - let resultsPerPage = parseInt(req.query.resultsPerPage, 10) || 50; - if (![50, 100, 250, 500].includes(resultsPerPage)) { - resultsPerPage = 50; - } - const start = Math.max(0, page - 1) * resultsPerPage; - const stop = start + resultsPerPage - 1; - const byScore = min !== undefined && max !== undefined; - - async function getCount() { - if (byScore) { - return await db.sortedSetCount(set, min, max); - } else if (set === 'users:banned' || set === 'users:notvalidated') { - return await db.sortedSetCard(set); - } - return await db.getObjectField('global', 'userCount'); - } - - async function getUsersWithFields() { - let uids; - if (byScore) { - uids = await db.getSortedSetRevRangeByScore(set, start, resultsPerPage, max, min); - } else { - uids = await user.getUidsFromSet(set, start, stop); - } - const [isAdmin, userData] = await Promise.all([ - user.isAdministrator(uids), - user.getUsersWithFields(uids, userFields, req.uid), - ]); - userData.forEach((user, index) => { - if (user) { - user.administrator = isAdmin[index]; - } - }); - return userData; - } - - const [count, users] = await Promise.all([ - getCount(), - getUsersWithFields(), - ]); - - const data = { - users: users.filter(user => user && parseInt(user.uid, 10)), - page: page, - pageCount: Math.max(1, Math.ceil(count / resultsPerPage)), - resultsPerPage: resultsPerPage, - }; - data[section] = true; - render(req, res, data); -} - function render(req, res, data) { - data.search_display = 'hidden'; data.pagination = pagination.create(data.page, data.pageCount, req.query); - data.requireEmailConfirmation = meta.config.requireEmailConfirmation; - var registrationType = meta.config.registrationType; + const registrationType = meta.config.registrationType; data.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only'; data.adminInviteOnly = registrationType === 'admin-invite-only'; - + data['sort_' + data.sortBy] = true; + if (req.query.searchBy) { + data['searchBy_' + validator.escape(String(req.query.searchBy))] = true; + } + const filterBy = Array.isArray(req.query.filters || []) ? (req.query.filters || []) : [req.query.filters]; + filterBy.forEach(function (filter) { + data['filterBy_' + validator.escape(String(filter))] = true; + }); res.render('admin/manage/users', data); } diff --git a/src/controllers/users.js b/src/controllers/users.js index c485e8ecff..3ead286208 100644 --- a/src/controllers/users.js +++ b/src/controllers/users.js @@ -21,7 +21,7 @@ usersController.index = async function (req, res, next) { flagged: usersController.getFlaggedUsers, }; - if (req.query.term) { + if (req.query.query) { await usersController.search(req, res, next); } else if (sectionToController[section]) { await sectionToController[section](req, res, next); @@ -35,19 +35,25 @@ usersController.search = async function (req, res) { privileges.global.can('search:users', req.uid), user.isPrivileged(req.uid), ]); - - if (!allowed || ((req.query.searchBy === 'ip' || req.query.searchBy === 'email' || req.query.bannedOnly === 'true' || req.query.flaggedOnly === 'true') && !isPrivileged)) { + let filters = req.query.filters || []; + filters = Array.isArray(filters) ? filters : [filters]; + if (!allowed || + (( + req.query.searchBy === 'ip' || + req.query.searchBy === 'email' || + filters.includes('banned') || + filters.includes('flagged') + ) && !isPrivileged) + ) { throw new Error('[[error:no-privileges]]'); } const [searchData, isAdminOrGlobalMod] = await Promise.all([ user.search({ - query: req.query.term, + query: req.query.query, searchBy: req.query.searchBy || 'username', page: req.query.page || 1, sortBy: req.query.sortBy || 'joindate', - onlineOnly: req.query.onlineOnly === 'true', - bannedOnly: req.query.bannedOnly === 'true', - flaggedOnly: req.query.flaggedOnly === 'true', + filters: filters, }), user.isAdminOrGlobalMod(req.uid), ]); diff --git a/src/controllers/write/groups.js b/src/controllers/write/groups.js index 9f95f5361a..a8b16e7136 100644 --- a/src/controllers/write/groups.js +++ b/src/controllers/write/groups.js @@ -70,7 +70,7 @@ Groups.join = async (req, res) => { if (!res.locals.privileges.isAdmin) { // Admin and privilege groups unjoinable client-side - if (group.name === 'administrators' || groups.isPrivilegeGroup(group.name)) { + if (groups.systemGroups.includes(group.name) || groups.isPrivilegeGroup(group.name)) { throw new Error('[[error:not-allowed]]'); } diff --git a/src/groups/create.js b/src/groups/create.js index 20fe4ae465..8260e24fa8 100644 --- a/src/groups/create.js +++ b/src/groups/create.js @@ -67,7 +67,7 @@ module.exports = function (Groups) { function isSystemGroup(data) { return data.system === true || parseInt(data.system, 10) === 1 || - data.name === 'administrators' || data.name === 'registered-users' || data.name === 'Global Moderators' || + Groups.systemGroups.includes(data.name) || Groups.isPrivilegeGroup(data.name); } diff --git a/src/groups/index.js b/src/groups/index.js index bbcb0cde57..4140e558f9 100644 --- a/src/groups/index.js +++ b/src/groups/index.js @@ -25,14 +25,22 @@ require('./cache')(Groups); Groups.ephemeralGroups = ['guests', 'spiders']; +Groups.systemGroups = [ + 'registered-users', + 'verified-users', + 'unverified-users', + 'administrators', + 'Global Moderators', +]; + Groups.getEphemeralGroup = function (groupName) { return { name: groupName, slug: slugify(groupName), description: '', - deleted: '0', - hidden: '0', - system: '1', + deleted: 0, + hidden: 0, + system: 1, }; }; diff --git a/src/groups/join.js b/src/groups/join.js index 06519ce57b..09d94954a4 100644 --- a/src/groups/join.js +++ b/src/groups/join.js @@ -82,7 +82,7 @@ module.exports = function (Groups) { }); } catch (err) { if (err && err.message !== '[[error:group-already-exists]]') { - winston.error('[groups.join] Could not create new hidden group', err.stack); + winston.error('[groups.join] Could not create new hidden group (' + groupName + ')\n' + err.stack); throw err; } } @@ -90,7 +90,10 @@ module.exports = function (Groups) { } async function setGroupTitleIfNotSet(groupNames, uid) { - groupNames = groupNames.filter(groupName => groupName !== 'registered-users' && !Groups.isPrivilegeGroup(groupName)); + const ignore = ['registered-users', 'verified-users', 'unverified-users']; + groupNames = groupNames.filter( + groupName => !ignore.includes(groupName) && !Groups.isPrivilegeGroup(groupName) + ); if (!groupNames.length) { return; } diff --git a/src/messaging/edit.js b/src/messaging/edit.js index 303ff5a2e1..ca1e5a80eb 100644 --- a/src/messaging/edit.js +++ b/src/messaging/edit.js @@ -3,6 +3,7 @@ const meta = require('../meta'); const user = require('../user'); const plugins = require('../plugins'); +const privileges = require('../privileges'); const sockets = require('../socket.io'); @@ -52,12 +53,13 @@ module.exports = function (Messaging) { throw new Error('[[error:chat-message-editing-disabled]]'); } - const userData = await user.getUserFields(uid, ['banned', 'email:confirmed']); + const userData = await user.getUserFields(uid, ['banned']); if (userData.banned) { throw new Error('[[error:user-banned]]'); } - if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) { - throw new Error('[[error:email-not-confirmed]]'); + const canChat = await privileges.global.can('chat', uid); + if (!canChat) { + throw new Error('[[error:no-privileges]]'); } const [isAdmin, messageData] = await Promise.all([ diff --git a/src/messaging/index.js b/src/messaging/index.js index 9a05e682e9..757884e9a8 100644 --- a/src/messaging/index.js +++ b/src/messaging/index.js @@ -5,6 +5,7 @@ const validator = require('validator'); const db = require('../database'); const user = require('../user'); +const privileges = require('../privileges'); const plugins = require('../plugins'); const meta = require('../meta'); const utils = require('../utils'); @@ -201,13 +202,13 @@ Messaging.canMessageUser = async (uid, toUid) => { throw new Error('[[error:no-user]]'); } - const userData = await user.getUserFields(uid, ['banned', 'email:confirmed']); + const userData = await user.getUserFields(uid, ['banned']); if (userData.banned) { throw new Error('[[error:user-banned]]'); } - - if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) { - throw new Error('[[error:email-not-confirmed-chat]]'); + const canChat = await privileges.global.can('chat', uid); + if (!canChat) { + throw new Error('[[error:no-privileges]]'); } const results = await utils.promiseParallel({ @@ -237,13 +238,13 @@ Messaging.canMessageRoom = async (uid, roomId) => { throw new Error('[[error:not-in-room]]'); } - const userData = await user.getUserFields(uid, ['banned', 'email:confirmed']); + const userData = await user.getUserFields(uid, ['banned']); if (userData.banned) { throw new Error('[[error:user-banned]]'); } - - if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) { - throw new Error('[[error:email-not-confirmed-chat]]'); + const canChat = await privileges.global.can('chat', uid); + if (!canChat) { + throw new Error('[[error:no-privileges]]'); } await plugins.fireHook('static:messaging.canMessageRoom', { diff --git a/src/privileges/admin.js b/src/privileges/admin.js index a242405ec0..aff1208248 100644 --- a/src/privileges/admin.js +++ b/src/privileges/admin.js @@ -79,7 +79,6 @@ module.exports = function (privileges) { 'admin.user.deleteUsers': 'admin:users', 'admin.user.deleteUsersAndContent': 'admin:users', 'admin.user.createUser': 'admin:users', - 'admin.user.search': 'admin:users', 'admin.user.invite': 'admin:users', 'admin.getSearchDict': 'admin:settings', diff --git a/src/privileges/helpers.js b/src/privileges/helpers.js index 93e189f6a9..ab596bf395 100644 --- a/src/privileges/helpers.js +++ b/src/privileges/helpers.js @@ -119,13 +119,15 @@ helpers.getGroupPrivileges = async function (cid, groupPrivileges) { groupNames = groups.ephemeralGroups.concat(groupNames); moveToFront(groupNames, 'Global Moderators'); + moveToFront(groupNames, 'unverified-users'); + moveToFront(groupNames, 'verified-users'); moveToFront(groupNames, 'registered-users'); const adminIndex = groupNames.indexOf('administrators'); if (adminIndex !== -1) { groupNames.splice(adminIndex, 1); } - const groupData = await groups.getGroupsFields(groupNames, ['private']); + const groupData = await groups.getGroupsFields(groupNames, ['private', 'system']); const memberData = groupNames.map(function (member, index) { const memberPrivs = {}; @@ -137,6 +139,7 @@ helpers.getGroupPrivileges = async function (cid, groupPrivileges) { nameEscaped: translator.escape(validator.escape(member)), privileges: memberPrivs, isPrivate: groupData[index] && !!groupData[index].private, + isSystem: groupData[index] && !!groupData[index].system, }; }); return memberData; diff --git a/src/routes/admin.js b/src/routes/admin.js index 8eb379d127..e528a15068 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -16,16 +16,7 @@ module.exports = function (app, middleware, controllers) { helpers.setupAdminPageRoute(app, '/admin/manage/privileges/:cid?', middleware, middlewares, controllers.admin.privileges.get); helpers.setupAdminPageRoute(app, '/admin/manage/tags', middleware, middlewares, controllers.admin.tags.get); - helpers.setupAdminPageRoute(app, '/admin/manage/users', middleware, middlewares, controllers.admin.users.sortByJoinDate); - helpers.setupAdminPageRoute(app, '/admin/manage/users/search', middleware, middlewares, controllers.admin.users.search); - helpers.setupAdminPageRoute(app, '/admin/manage/users/latest', middleware, middlewares, controllers.admin.users.sortByJoinDate); - helpers.setupAdminPageRoute(app, '/admin/manage/users/not-validated', middleware, middlewares, controllers.admin.users.notValidated); - helpers.setupAdminPageRoute(app, '/admin/manage/users/no-posts', middleware, middlewares, controllers.admin.users.noPosts); - helpers.setupAdminPageRoute(app, '/admin/manage/users/top-posters', middleware, middlewares, controllers.admin.users.topPosters); - helpers.setupAdminPageRoute(app, '/admin/manage/users/most-reputation', middleware, middlewares, controllers.admin.users.mostReputaion); - helpers.setupAdminPageRoute(app, '/admin/manage/users/inactive', middleware, middlewares, controllers.admin.users.inactive); - helpers.setupAdminPageRoute(app, '/admin/manage/users/flagged', middleware, middlewares, controllers.admin.users.flagged); - helpers.setupAdminPageRoute(app, '/admin/manage/users/banned', middleware, middlewares, controllers.admin.users.banned); + helpers.setupAdminPageRoute(app, '/admin/manage/users', middleware, middlewares, controllers.admin.users.index); helpers.setupAdminPageRoute(app, '/admin/manage/registration', middleware, middlewares, controllers.admin.users.registrationQueue); helpers.setupAdminPageRoute(app, '/admin/manage/admins-mods', middleware, middlewares, controllers.admin.adminsMods.get); diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js index 990869bb7e..c63032273f 100644 --- a/src/socket.io/admin/user.js +++ b/src/socket.io/admin/user.js @@ -80,7 +80,10 @@ User.validateEmail = async function (socket, uids) { uids = uids.filter(uid => parseInt(uid, 10)); await db.setObjectField(uids.map(uid => 'user:' + uid), 'email:confirmed', 1); - await db.sortedSetRemove('users:notvalidated', uids); + for (const uid of uids) { + await groups.join('verified-users', uid); + await groups.leave('unverified-users', uid); + } }; User.sendValidationEmail = async function (socket, uids) { @@ -183,32 +186,6 @@ async function deleteUsers(socket, uids, method) { } } -User.search = async function (socket, data) { - // TODO: deprecate - const searchData = await user.search({ - query: data.query, - searchBy: data.searchBy, - uid: socket.uid, - }); - - if (!searchData.users.length) { - return searchData; - } - - const uids = searchData.users.map(user => user && user.uid); - const userInfo = await user.getUsersFields(uids, ['email', 'flags', 'lastonline', 'joindate']); - - searchData.users.forEach(function (user, index) { - if (user && userInfo[index]) { - user.email = userInfo[index].email; - user.flags = userInfo[index].flags || 0; - user.lastonlineISO = userInfo[index].lastonlineISO; - user.joindateISO = userInfo[index].joindateISO; - } - }); - return searchData; -}; - User.restartJobs = async function () { user.startJobs(); }; diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index e46983e183..9aebf738cf 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -30,7 +30,7 @@ SocketGroups.join = async (socket, data) => { throw new Error('[[error:invalid-group-name]]'); } - if (data.groupName === 'administrators' || groups.isPrivilegeGroup(data.groupName)) { + if (groups.systemGroups.includes(data.groupName) || groups.isPrivilegeGroup(data.groupName)) { throw new Error('[[error:not-allowed]]'); } diff --git a/src/socket.io/user/search.js b/src/socket.io/user/search.js index b1fc2e1e3e..c181fb599d 100644 --- a/src/socket.io/user/search.js +++ b/src/socket.io/user/search.js @@ -6,6 +6,7 @@ const privileges = require('../../privileges'); module.exports = function (SocketUser) { SocketUser.search = async function (socket, data) { + // TODO: depracate and use usersController.search if (!data) { throw new Error('[[error:invalid-data]]'); } @@ -14,7 +15,16 @@ module.exports = function (SocketUser) { user.isPrivileged(socket.uid), ]); - if (!allowed || ((data.searchBy === 'ip' || data.searchBy === 'email' || data.bannedOnly || data.flaggedOnly) && !isPrivileged)) { + let filters = data.filters || []; + filters = Array.isArray(filters) ? filters : [filters]; + if (!allowed || + (( + data.searchBy === 'ip' || + data.searchBy === 'email' || + filters.includes('banned') || + filters.includes('flagged') + ) && !isPrivileged) + ) { throw new Error('[[error:no-privileges]]'); } const result = await user.search({ @@ -22,10 +32,7 @@ module.exports = function (SocketUser) { page: data.page, searchBy: data.searchBy, sortBy: data.sortBy, - onlineOnly: data.onlineOnly, - bannedOnly: data.bannedOnly, - notBanned: data.notBanned, - flaggedOnly: data.flaggedOnly, + filters: data.filters, paginate: data.paginate, uid: socket.uid, }); diff --git a/src/upgrade.js b/src/upgrade.js index 0ab82a135f..2136913c68 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -129,6 +129,7 @@ Upgrade.process = async function (files, skipCount) { const version = path.dirname(file).split('/').pop(); const progress = { current: 0, + counter: 0, total: 0, incr: Upgrade.incrementProgress, script: scriptExport, @@ -177,9 +178,11 @@ Upgrade.incrementProgress = function (value) { } this.current += value || 1; + this.counter += value || 1; + const step = (this.total ? Math.floor(this.total / 100) : 100); - // Redraw the progress bar every 100 units - if (this.current % (this.total ? Math.floor(this.total / 100) : 100) === 0 || this.current === this.total) { + if (this.counter > step || this.current >= this.total) { + this.counter -= step; var percentage = 0; var filled = 0; var unfilled = 15; diff --git a/src/upgrades/1.15.0/verified_users_group.js b/src/upgrades/1.15.0/verified_users_group.js new file mode 100644 index 0000000000..7e5bcd4c64 --- /dev/null +++ b/src/upgrades/1.15.0/verified_users_group.js @@ -0,0 +1,95 @@ +'use strict'; + +const db = require('../../database'); + +const batch = require('../../batch'); +const user = require('../../user'); +const groups = require('../../groups'); +const meta = require('../../meta'); +const privileges = require('../../privileges'); + +module.exports = { + name: 'Create verified/unverified user groups', + timestamp: Date.UTC(2020, 9, 13), + method: async function () { + const progress = this.progress; + const timestamp = await db.getObjectField('group:administrators', 'timestamp'); + const verifiedExists = await groups.exists('verified-users'); + if (!verifiedExists) { + await groups.create({ + name: 'verified-users', + hidden: 1, + private: 1, + system: 1, + disableLeave: 1, + disableJoinRequests: 1, + timestamp: timestamp + 1, + }); + } + const unverifiedExists = await groups.exists('unverified-users'); + if (!unverifiedExists) { + await groups.create({ + name: 'unverified-users', + hidden: 1, + private: 1, + system: 1, + disableLeave: 1, + disableJoinRequests: 1, + timestamp: timestamp + 1, + }); + } + + await batch.processSortedSet('users:joindate', async function (uids) { + progress.incr(uids.length); + const userData = await user.getUsersFields(uids, ['uid', 'email:confirmed']); + + const verified = userData.filter(u => parseInt(u['email:confirmed'], 10) === 1); + const unverified = userData.filter(u => parseInt(u['email:confirmed'], 10) !== 1); + + for (const user of verified) { + // eslint-disable-next-line no-await-in-loop + await groups.join('verified-users', user.uid); + } + for (const user of unverified) { + // eslint-disable-next-line no-await-in-loop + await groups.join('unverified-users', user.uid); + } + }, { + batch: 500, + progress: this.progress, + }); + + await db.delete('users:notvalidated'); + await updatePrivilges(); + }, +}; + +async function updatePrivilges() { + // if email confirmation is required + // give chat, posting privs to "verified-users" group + // remove chat, posting privs from "registered-users" group + if (meta.config.requireEmailConfirmation) { + const cids = await db.getSortedSetRevRange('categories:cid', 0, -1); + const canChat = await privileges.global.canGroup('chat', 'registered-users'); + if (canChat) { + await privileges.global.give(['groups:chat'], 'verified-users'); + await privileges.global.rescind(['groups:chat'], 'registered-users'); + } + for (const cid of cids) { + /* eslint-disable no-await-in-loop */ + const data = await privileges.categories.list(cid); + + const registeredUsersPrivs = data.groups.find(d => d.name === 'registered-users').privileges; + + if (registeredUsersPrivs['groups:topics:create']) { + await privileges.categories.give(['groups:topics:create'], cid, 'verified-users'); + await privileges.categories.rescind(['groups:topics:create'], cid, 'registered-users'); + } + + if (registeredUsersPrivs['groups:topics:reply']) { + await privileges.categories.give(['groups:topics:reply'], cid, 'verified-users'); + await privileges.categories.rescind(['groups:topics:reply'], cid, 'registered-users'); + } + } + } +} diff --git a/src/user/create.js b/src/user/create.js index e0cf8f18ca..69caf97877 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -86,9 +86,6 @@ module.exports = function (User) { ['users:reputation', 0, userData.uid], ]; - if (parseInt(userData.uid, 10) !== 1) { - bulkAdd.push(['users:notvalidated', timestamp, userData.uid]); - } if (userData.email) { bulkAdd.push(['email:uid', userData.uid, userData.email.toLowerCase()]); bulkAdd.push(['email:sorted', 0, userData.email.toLowerCase() + ':' + userData.uid]); @@ -99,10 +96,15 @@ module.exports = function (User) { bulkAdd.push(['fullname:sorted', 0, userData.fullname.toLowerCase() + ':' + userData.uid]); } + const groupsToJoin = ['registered-users'].concat( + parseInt(userData.uid, 10) !== 1 ? + 'unverified-users' : 'verified-users' + ); + await Promise.all([ db.incrObjectField('global', 'userCount'), db.sortedSetAddBulk(bulkAdd), - groups.join('registered-users', userData.uid), + groups.join(groupsToJoin, userData.uid), User.notifications.sendWelcomeNotification(userData.uid), storePassword(userData.uid, data.password), User.updateDigestSetting(userData.uid, meta.config.dailyDigestFreq), diff --git a/src/user/delete.js b/src/user/delete.js index e5d2a6850e..96dd6029c8 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -81,7 +81,6 @@ module.exports = function (User) { 'users:banned:expire', 'users:flags', 'users:online', - 'users:notvalidated', 'digest:day:uids', 'digest:week:uids', 'digest:month:uids', diff --git a/src/user/email.js b/src/user/email.js index 9e75a40f48..9fab6c9ca2 100644 --- a/src/user/email.js +++ b/src/user/email.js @@ -9,6 +9,7 @@ var plugins = require('../plugins'); var db = require('../database'); var meta = require('../meta'); var emailer = require('../emailer'); +const groups = require('../groups'); var UserEmail = module.exports; @@ -96,8 +97,9 @@ UserEmail.confirm = async function (code) { throw new Error('[[error:invalid-email]]'); } await user.setUserField(confirmObj.uid, 'email:confirmed', 1); + await groups.join('verified-users', confirmObj.uid); + await groups.leave('unverified-users', confirmObj.uid); await db.delete('confirm:' + code); await db.delete('uid:' + confirmObj.uid + ':confirm:email:sent'); - await db.sortedSetRemove('users:notvalidated', confirmObj.uid); await plugins.fireHook('action:user.email.confirmed', { uid: confirmObj.uid, email: confirmObj.email }); }; diff --git a/src/user/posts.js b/src/user/posts.js index e7e9174845..9f85b3456a 100644 --- a/src/user/posts.js +++ b/src/user/posts.js @@ -18,7 +18,7 @@ module.exports = function (User) { return; } const [userData, isAdminOrMod] = await Promise.all([ - User.getUserFields(uid, ['uid', 'banned', 'joindate', 'email', 'email:confirmed', 'reputation'].concat([field])), + User.getUserFields(uid, ['uid', 'banned', 'joindate', 'email', 'reputation'].concat([field])), privileges.categories.isAdminOrMod(cid, uid), ]); @@ -34,16 +34,12 @@ module.exports = function (User) { throw new Error('[[error:user-banned]]'); } - if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) { - throw new Error('[[error:email-not-confirmed]]'); - } - - var now = Date.now(); + const now = Date.now(); if (now - userData.joindate < meta.config.initialPostDelay * 1000) { throw new Error('[[error:user-too-new, ' + meta.config.initialPostDelay + ']]'); } - var lasttime = userData[field] || 0; + const lasttime = userData[field] || 0; if (meta.config.newbiePostDelay > 0 && meta.config.newbiePostDelayThreshold > userData.reputation && now - lasttime < meta.config.newbiePostDelay * 1000) { throw new Error('[[error:too-many-posts-newbie, ' + meta.config.newbiePostDelay + ', ' + meta.config.newbiePostDelayThreshold + ']]'); diff --git a/src/user/profile.js b/src/user/profile.js index 00e8970c35..e8f6e5e600 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -234,9 +234,10 @@ module.exports = function (User) { ['email:uid', uid, newEmail.toLowerCase()], ['email:sorted', 0, newEmail.toLowerCase() + ':' + uid], ['user:' + uid + ':emails', Date.now(), newEmail + ':' + Date.now()], - ['users:notvalidated', Date.now(), uid], ]), User.setUserFields(uid, { email: newEmail, 'email:confirmed': 0 }), + groups.leave('verified-users', uid), + groups.join('unverified-users', uid), User.reset.cleanByUid(uid), ]); diff --git a/src/user/reset.js b/src/user/reset.js index d8765b9401..91ac9627da 100644 --- a/src/user/reset.js +++ b/src/user/reset.js @@ -4,6 +4,7 @@ var nconf = require('nconf'); var winston = require('winston'); var user = require('./index'); +const groups = require('../groups'); var utils = require('../utils'); var batch = require('../batch'); @@ -70,11 +71,12 @@ UserReset.commit = async function (code, password) { const hash = await user.hashPassword(password); await user.setUserFields(uid, { password: hash, 'email:confirmed': 1 }); + await groups.join('verified-users', uid); + await groups.leave('unverified-users', uid); await db.deleteObjectField('reset:uid', code); await db.sortedSetRemoveBulk([ ['reset:issueDate', code], ['reset:issueDate:uid', uid], - ['users:notvalidated', uid], ]); await user.reset.updateExpiry(uid); await user.auth.resetLockout(uid); diff --git a/src/user/search.js b/src/user/search.js index e2e94b19f6..32c9d6bd2f 100644 --- a/src/user/search.js +++ b/src/user/search.js @@ -10,6 +10,25 @@ const groups = require('../groups'); const utils = require('../utils'); module.exports = function (User) { + const filterFnMap = { + online: user => user.status !== 'offline' && (Date.now() - user.lastonline < 300000), + banned: user => user.banned, + notbanned: user => !user.banned, + flagged: user => parseInt(user.flags, 10) > 0, + verified: user => !!user['email:confirmed'], + unverified: user => !user['email:confirmed'], + }; + + const filterFieldMap = { + online: ['status', 'lastonline'], + banned: ['banned'], + notbanned: ['banned'], + flagged: ['flags'], + verified: ['email:confirmed'], + unverified: ['email:confirmed'], + }; + + User.search = async function (data) { const query = data.query || ''; const searchBy = data.searchBy || 'username'; @@ -69,21 +88,19 @@ module.exports = function (User) { async function filterAndSortUids(uids, data) { uids = uids.filter(uid => parseInt(uid, 10)); - + let filters = data.filters || []; + filters = Array.isArray(filters) ? filters : [data.filters]; const fields = []; if (data.sortBy) { fields.push(data.sortBy); } - if (data.onlineOnly) { - fields.push('status', 'lastonline'); - } - if (data.bannedOnly || data.notBanned) { - fields.push('banned'); - } - if (data.flaggedOnly) { - fields.push('flags'); - } + + filters.forEach(function (filter) { + if (filterFieldMap[filter]) { + fields.push(...filterFieldMap[filter]); + } + }); if (data.groupName) { const isMembers = await groups.isMembers(uids, data.groupName); @@ -96,42 +113,36 @@ module.exports = function (User) { fields.push('uid'); let userData = await User.getUsersFields(uids, fields); - if (data.onlineOnly) { - userData = userData.filter(user => user.status !== 'offline' && (Date.now() - user.lastonline < 300000)); - } - - if (data.bannedOnly) { - userData = userData.filter(user => user.banned); - } - if (data.notBanned) { - userData = userData.filter(user => !user.banned); - } - - if (data.flaggedOnly) { - userData = userData.filter(user => parseInt(user.flags, 10) > 0); - } + filters.forEach(function (filter) { + if (filterFnMap[filter]) { + userData = userData.filter(filterFnMap[filter]); + } + }); if (data.sortBy) { - sortUsers(userData, data.sortBy); + sortUsers(userData, data.sortBy, data.sortDirection); } return userData.map(user => user.uid); } - function sortUsers(userData, sortBy) { + function sortUsers(userData, sortBy, sortDirection) { if (!userData || !userData.length) { return; } + sortDirection = sortDirection || 'desc'; + const direction = sortDirection === 'desc' ? 1 : -1; + const isNumeric = utils.isNumber(userData[0][sortBy]); if (isNumeric) { - userData.sort((u1, u2) => u2[sortBy] - u1[sortBy]); + userData.sort((u1, u2) => direction * (u2[sortBy] - u1[sortBy])); } else { userData.sort(function (u1, u2) { if (u1[sortBy] < u2[sortBy]) { - return -1; + return direction * -1; } else if (u1[sortBy] > u2[sortBy]) { - return 1; + return direction * 1; } return 0; }); diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl index f31382e671..bb08b56ecb 100644 --- a/src/views/admin/manage/users.tpl +++ b/src/views/admin/manage/users.tpl @@ -2,51 +2,70 @@