diff --git a/public/src/admin/advanced/events.js b/public/src/admin/advanced/events.js index 2be2c44ee7..0645f8e799 100644 --- a/public/src/admin/advanced/events.js +++ b/public/src/admin/advanced/events.js @@ -13,6 +13,11 @@ define('admin/advanced/events', function () { $('.events-list').empty(); }); }); + + $('#filter').on('change', function () { + var filter = $(this).val(); + ajaxify.go('admin/advanced/events' + (filter ? '?filter=' + filter : '')); + }); }; return Events; diff --git a/src/controllers/admin/events.js b/src/controllers/admin/events.js index f6988ef72e..88a2848f37 100644 --- a/src/controllers/admin/events.js +++ b/src/controllers/admin/events.js @@ -14,24 +14,35 @@ eventsController.get = function (req, res, next) { var start = (page - 1) * itemsPerPage; var stop = start + itemsPerPage - 1; + var currentFilter = req.query.filter || ''; + async.waterfall([ function (next) { async.parallel({ eventCount: function (next) { - db.sortedSetCard('events:time', next); + db.sortedSetCard('events:time' + (currentFilter ? ':' + currentFilter : ''), next); }, events: function (next) { - events.getEvents(start, stop, next); + events.getEvents(currentFilter, start, stop, next); }, }, next); }, function (results) { + var types = [''].concat(events.types); + var filters = types.map(function (type) { + return { + value: type, + name: type || 'all', + selected: type === currentFilter, + }; + }); + var pageCount = Math.max(1, Math.ceil(results.eventCount / itemsPerPage)); res.render('admin/advanced/events', { events: results.events, - pagination: pagination.create(page, pageCount), - next: 20, + pagination: pagination.create(page, pageCount, req.query), + filters: filters, }); }, ], next); diff --git a/src/events.js b/src/events.js index cb8798ed70..5ba1546a40 100644 --- a/src/events.js +++ b/src/events.js @@ -4,6 +4,7 @@ var async = require('async'); var validator = require('validator'); var winston = require('winston'); +var _ = require('lodash'); var db = require('./database'); var batch = require('./batch'); @@ -12,6 +13,39 @@ var utils = require('./utils'); var events = module.exports; +events.types = [ + 'plugin-activate', + 'plugin-deactivate', + 'restart', + 'build', + 'config-change', + 'settings-change', + 'category-purge', + 'privilege-change', + 'post-delete', + 'post-restore', + 'post-purge', + 'topic-delete', + 'topic-restore', + 'topic-purge', + 'topic-rename', + 'password-reset', + 'user-ban', + 'user-unban', + 'user-delete', + 'password-change', + 'email-change', + 'username-change', + 'registration-approved', + 'registration-rejected', + 'accept-membership', + 'reject-membership', + 'theme-set', + 'export:uploads', + 'account-locked', + 'getUsersCSV', +]; + /** * Useful options in data: type, uid, ip, targetUid * Everything else gets stringified and shown as pretty JSON string @@ -31,6 +65,9 @@ events.log = function (data, callback) { function (next) { db.sortedSetAdd('events:time', data.timestamp, eid, next); }, + function (next) { + db.sortedSetAdd('events:time:' + data.type, data.timestamp, eid, next); + }, function (next) { db.setObject('event:' + eid, data, next); }, @@ -41,10 +78,10 @@ events.log = function (data, callback) { }); }; -events.getEvents = function (start, stop, callback) { +events.getEvents = function (filter, start, stop, callback) { async.waterfall([ function (next) { - db.getSortedSetRevRange('events:time', start, stop, next); + db.getSortedSetRevRange('events:time' + (filter ? ':' + filter : ''), start, stop, next); }, function (eids, next) { var keys = eids.map(function (eid) { @@ -123,15 +160,24 @@ function addUserData(eventsData, field, objectName, callback) { events.deleteEvents = function (eids, callback) { callback = callback || function () {}; - async.parallel([ + var keys; + async.waterfall([ function (next) { - var keys = eids.map(function (eid) { + keys = eids.map(function (eid) { return 'event:' + eid; }); - db.deleteAll(keys, next); + db.getObjectsFields(keys, ['type'], next); }, - function (next) { - db.sortedSetRemove('events:time', eids, next); + function (eventData, next) { + var sets = _.uniq(['events:time'].concat(eventData.map(e => 'events:time:' + e.type))); + async.parallel([ + function (next) { + db.deleteAll(keys, next); + }, + function (next) { + db.sortedSetRemove(sets, eids, next); + }, + ], next); }, ], callback); }; @@ -146,7 +192,7 @@ events.deleteAll = function (callback) { events.output = function () { console.log('\nDisplaying last ten administrative events...'.bold); - events.getEvents(0, 9, function (err, events) { + events.getEvents('', 0, 9, function (err, events) { if (err) { winston.error('Error fetching events', err); throw err; diff --git a/src/socket.io/admin/categories.js b/src/socket.io/admin/categories.js index d35a582691..4bab1330d5 100644 --- a/src/socket.io/admin/categories.js +++ b/src/socket.io/admin/categories.js @@ -84,8 +84,10 @@ Categories.setPrivilege = function (socket, data, callback) { function onSetComplete() { events.log({ uid: socket.uid, + type: 'privilege-change', ip: socket.ip, privilege: data.privilege.toString(), + cid: data.cid, action: data.set ? 'grant' : 'rescind', target: data.member, }, callback); diff --git a/src/upgrades/1.10.2/event_filters.js b/src/upgrades/1.10.2/event_filters.js new file mode 100644 index 0000000000..4f2e87485e --- /dev/null +++ b/src/upgrades/1.10.2/event_filters.js @@ -0,0 +1,46 @@ +'use strict'; + +var db = require('../../database'); + +var async = require('async'); +var batch = require('../../batch'); + +module.exports = { + name: 'add filters to events', + timestamp: Date.UTC(2018, 9, 4), + method: function (callback) { + const progress = this.progress; + + batch.processSortedSet('events:time', function (eids, next) { + async.eachSeries(eids, function (eid, next) { + progress.incr(); + + db.getObject('event:' + eid, function (err, eventData) { + if (err) { + return next(err); + } + if (!eventData) { + return db.sortedSetRemove('events:time', eid, next); + } + // privilege events we're missing type field + if (!eventData.type && eventData.privilege) { + eventData.type = 'privilege-change'; + async.waterfall([ + function (next) { + db.setObjectField('event:' + eid, 'type', 'privilege-change', next); + }, + function (next) { + db.sortedSetAdd('events:time:' + eventData.type, eventData.timestamp, eid, next); + }, + ], next); + return; + } + + db.sortedSetAdd('events:time:' + (eventData.type || ''), eventData.timestamp, eid, next); + }); + }, next); + }, { + progress: this.progress, + }, callback); + }, +}; diff --git a/src/views/admin/advanced/events.tpl b/src/views/admin/advanced/events.tpl index 65c707920f..4372d12b93 100644 --- a/src/views/admin/advanced/events.tpl +++ b/src/views/admin/advanced/events.tpl @@ -1,8 +1,13 @@
+
[[admin/advanced/events:events]]
-
+
[[admin/advanced/events:no-events]]