From 1354739d199586844b0465ada61e513bf4a2d22e Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 14 Mar 2014 19:07:50 -0400 Subject: [PATCH] user deletion #746 user deletion NOT SKALABLE --- public/src/forum/admin/users.js | 49 ++--- src/admin/user.js | 38 ++-- src/categories/activeusers.js | 4 +- src/controllers/topics.js | 6 +- src/events.js | 49 +++-- src/favourites.js | 8 +- src/groups.js | 4 + src/socket.io/admin.js | 4 + src/threadTools.js | 2 +- src/topics.js | 8 + src/user.js | 2 +- src/user/admin.js | 328 +++++++++++++++++++++++++++++++- 12 files changed, 435 insertions(+), 67 deletions(-) diff --git a/public/src/forum/admin/users.js b/public/src/forum/admin/users.js index d3ade3d363..9c2af40ce5 100644 --- a/public/src/forum/admin/users.js +++ b/public/src/forum/admin/users.js @@ -25,15 +25,9 @@ define(function() { elements.each(function(index, element) { var banBtn = $(element); var uid = getUID(banBtn); - if (isUserAdmin(banBtn) || uid === yourid) { - banBtn.addClass('disabled'); - } else if (isUserBanned(banBtn)) { - banBtn.addClass('btn-warning'); - } else if (!isUserAdmin(banBtn)) { - banBtn.removeClass('disabled'); - } else { - banBtn.removeClass('btn-warning'); - } + + banBtn.toggleClass('disabled', isUserAdmin(banBtn) || uid === yourid); + banBtn.toggleClass('btn-warning', isUserBanned(banBtn)); }); } @@ -41,18 +35,9 @@ define(function() { elements.each(function(index, element) { var adminBtn = $(element); var uid = getUID(adminBtn); - if (isUserAdmin(adminBtn)) { - adminBtn.attr('value', 'UnMake Admin').html('Remove Admin'); - if (uid === yourid) { - adminBtn.addClass('disabled'); - } - } else if (isUserBanned(adminBtn)) { - adminBtn.addClass('disabled'); - } else if (!isUserBanned(adminBtn)) { - adminBtn.removeClass('disabled'); - } else { - adminBtn.removeClass('btn-warning'); - } + + adminBtn.toggleClass('disabled', (isUserAdmin(adminBtn) && uid === yourid) || isUserBanned(adminBtn)); + adminBtn.toggleClass('btn-success', isUserAdmin(adminBtn)); }); } @@ -101,23 +86,39 @@ define(function() { }); } else if (!isUserAdmin(adminBtn)) { socket.emit('admin.user.makeAdmin', uid); - adminBtn.attr('value', 'UnMake Admin').html('Remove Admin'); parent.attr('data-admin', 1); updateUserBanButtons($('.ban-btn')); - + updateUserAdminButtons($('.admin-btn')); } else if(uid !== yourid) { bootbox.confirm('Do you really want to remove this user as admin "' + parent.attr('data-username') + '"?', function(confirm) { if (confirm) { socket.emit('admin.user.removeAdmin', uid); - adminBtn.attr('value', 'Make Admin').html('Make Admin'); parent.attr('data-admin', 0); updateUserBanButtons($('.ban-btn')); + updateUserAdminButtons($('.admin-btn')); } }); } return false; }); + $('#users-container').on('click', '.delete-btn', function() { + var deleteBtn = $(this); + var parent = deleteBtn.parents('.users-box'); + var uid = getUID(deleteBtn); + bootbox.confirm('Warning!
Do you really want to delete this user "' + parent.attr('data-username') + '"?
This action is not reversable, all user data and content will be erased!', function(confirm) { + if (confirm) { + socket.emit('admin.user.deleteUser', uid, function(err) { + if (err) { + return app.alertError(err.message); + } + parent.remove(); + app.alertSuccess('User Deleted!'); + }); + } + }); + }); + function handleUserCreate() { var errorEl = $('#create-modal-error'); $('#createUser').on('click', function() { diff --git a/src/admin/user.js b/src/admin/user.js index 36b8db4704..387988fa6b 100644 --- a/src/admin/user.js +++ b/src/admin/user.js @@ -1,4 +1,8 @@ -var utils = require('../../public/src/utils'), +'use strict'; + + +var async = require('async'), + utils = require('../../public/src/utils'), user = require('../user'), groups = require('../groups'); @@ -6,23 +10,13 @@ var utils = require('../../public/src/utils'), UserAdmin.createUser = function(uid, userData, callback) { user.isAdministrator(uid, function(err, isAdmin) { - if(err) { - return callback(err); + if(err || !isAdmin) { + return callback(err || new Error('You are not an administrator')); } - if (isAdmin) { - user.create(userData, function(err) { - if(err) { - return callback(err); - } - - callback(null); - }); - } else { - callback(new Error('You are not an administrator')); - } + user.create(userData, callback); }); - } + }; UserAdmin.makeAdmin = function(uid, theirid, socket) { user.isAdministrator(uid, function(err, isAdmin) { @@ -105,4 +99,18 @@ var utils = require('../../public/src/utils'), }); }; + UserAdmin.deleteUser = function(uid, theirid, callback) { + async.waterfall([ + function(next) { + user.isAdministrator(uid, next); + }, + function(isAdmin, next) { + if(!isAdmin) { + return next(new Error('You are not an administrator')); + } + user.delete(uid, theirid, next); + } + ], callback); + }; + }(exports)); \ No newline at end of file diff --git a/src/categories/activeusers.js b/src/categories/activeusers.js index 059432f3e5..7fd86651aa 100644 --- a/src/categories/activeusers.js +++ b/src/categories/activeusers.js @@ -49,8 +49,8 @@ module.exports = function(Categories) { } }; - Categories.removeActiveUser = function(cid, uid) { - db.sortedSetRemove('cid:' + cid + ':active_users', uid); + Categories.removeActiveUser = function(cid, uid, callback) { + db.sortedSetRemove('cid:' + cid + ':active_users', uid, callback); }; Categories.getActiveUsers = function(cid, callback) { diff --git a/src/controllers/topics.js b/src/controllers/topics.js index 8edf274e15..15452aaf80 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -145,9 +145,9 @@ topicsController.get = function(req, res, next) { ], function (err, data) { if (err) { if (err.message === 'not-enough-privileges') { - return res.redirect('403'); + return res.locals.isAPI ? res.json(403, err.message) : res.redirect('403'); } else { - return res.redirect('404'); + return res.locals.isAPI ? res.json(404, 'not-found') : res.redirect('404'); } } @@ -170,7 +170,7 @@ topicsController.get = function(req, res, next) { // Paginator for noscript data.pages = []; - for(var x=1;x<=data.pageCount;x++) { + for(var x=1; x<=data.pageCount; x++) { data.pages.push({ page: x, active: x === parseInt(page, 10) diff --git a/src/events.js b/src/events.js index 57b61c8aeb..3890b707e7 100644 --- a/src/events.js +++ b/src/events.js @@ -1,6 +1,8 @@ +'use strict'; var fs = require('fs'), + winston = require('winston'), path = require('path'), nconf = require('nconf'), user = require('./user'); @@ -11,54 +13,62 @@ var fs = require('fs'), events.logPasswordChange = function(uid) { logWithUser(uid, 'changed password'); - } + }; + + events.logAdminChangeUserPassword = function(adminUid, theirUid, callback) { + logAdminEvent(adminUid, theirUid, 'changed password of', callback); + }; + + events.logAdminUserDelete = function(adminUid, theirUid, callback) { + logAdminEvent(adminUid, theirUid, 'deleted', callback); + }; - events.logAdminChangeUserPassword = function(adminUid, theirUid) { + function logAdminEvent(adminUid, theirUid, message, callback) { user.getMultipleUserFields([adminUid, theirUid], ['username'], function(err, userData) { if(err) { return winston.error('Error logging event. ' + err.message); } - var msg = userData[0].username + '(uid ' + adminUid + ') changed password of ' + userData[1].username + '(uid ' + theirUid + ')'; - events.log(msg); + var msg = userData[0].username + '(uid ' + adminUid + ') ' + message + ' ' + userData[1].username + '(uid ' + theirUid + ')'; + events.log(msg, callback); }); } events.logPasswordReset = function(uid) { logWithUser(uid, 'reset password'); - } + }; events.logEmailChange = function(uid, oldEmail, newEmail) { logWithUser(uid,'changed email from "' + oldEmail + '" to "' + newEmail +'"'); - } + }; events.logUsernameChange = function(uid, oldUsername, newUsername) { logWithUser(uid,'changed username from "' + oldUsername + '" to "' + newUsername +'"'); - } + }; events.logAdminLogin = function(uid) { logWithUser(uid, 'logged into admin panel'); - } + }; events.logPostEdit = function(uid, pid) { logWithUser(uid, 'edited post (pid ' + pid + ')'); - } + }; events.logPostDelete = function(uid, pid) { logWithUser(uid, 'deleted post (pid ' + pid + ')'); - } + }; events.logPostRestore = function(uid, pid) { logWithUser(uid, 'restored post (pid ' + pid + ')'); - } + }; events.logTopicDelete = function(uid, tid) { logWithUser(uid, 'deleted topic (tid ' + tid + ')'); - } + }; events.logTopicRestore = function(uid, tid) { logWithUser(uid, 'restored topic (tid ' + tid + ')'); - } + }; function logWithUser(uid, string) { @@ -72,7 +82,7 @@ var fs = require('fs'), }); } - events.log = function(msg) { + events.log = function(msg, callback) { var logFile = path.join(nconf.get('base_dir'), logFileName); msg = '[' + new Date().toUTCString() + '] - ' + msg; @@ -80,10 +90,17 @@ var fs = require('fs'), fs.appendFile(logFile, msg + '\n', function(err) { if(err) { winston.error('Error logging event. ' + err.message); + if (typeof callback === 'function') { + callback(err); + } return; } + + if (typeof callback === 'function') { + callback(); + } }); - } + }; events.getLog = function(callback) { var logFile = path.join(nconf.get('base_dir'), logFileName); @@ -95,6 +112,6 @@ var fs = require('fs'), callback(null, 'No logs found'); } }); - } + }; }(module.exports)); \ No newline at end of file diff --git a/src/favourites.js b/src/favourites.js index 8084647bdd..2c164d1f28 100644 --- a/src/favourites.js +++ b/src/favourites.js @@ -224,9 +224,11 @@ var async = require('async'), }); } - socket.emit('posts.unfavourite', { - pid: pid - }); + if (socket) { + socket.emit('posts.unfavourite', { + pid: pid + }); + } } }); }); diff --git a/src/groups.js b/src/groups.js index db6ecdb81b..4b0ba0ea4a 100644 --- a/src/groups.js +++ b/src/groups.js @@ -7,6 +7,10 @@ user = require('./user'), db = require('./database'); + Groups.getGroupIds = function (callback) { + db.getObjectValues('group:gid', callback); + }; + Groups.list = function(options, callback) { db.getObjectValues('group:gid', function (err, gids) { if (gids.length > 0) { diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index 316d77a347..14093734ec 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -112,6 +112,10 @@ SocketAdmin.user.unbanUser = function(socket, theirid) { admin.user.unbanUser(socket.uid, theirid, socket); }; +SocketAdmin.user.deleteUser = function(socket, theirid, callback) { + admin.user.deleteUser(socket.uid, theirid, callback); +}; + SocketAdmin.user.search = function(socket, username, callback) { user.search(username, function(err, data) { function isAdmin(userData, next) { diff --git a/src/threadTools.js b/src/threadTools.js index 05d5f2d09a..415e2db5bb 100644 --- a/src/threadTools.js +++ b/src/threadTools.js @@ -311,7 +311,7 @@ var winston = require('winston'), return callback(err); } - if (pids.length === 0) { + if (!pids.length) { return callback(null, null); } diff --git a/src/topics.js b/src/topics.js index 32af5a6ebe..032e3ce440 100644 --- a/src/topics.js +++ b/src/topics.js @@ -695,7 +695,11 @@ var async = require('async'), privilegeCache = {}, userCache = {}; + function loadTopicInfo(topicData, next) { + if (!topicData) { + return next(null, null); + } function isTopicVisible(topicData, topicInfo) { var deleted = parseInt(topicData.deleted, 10) !== 0; @@ -736,6 +740,10 @@ var async = require('async'), categoryCache[topicData.cid] = topicInfo.categoryData; userCache[topicData.uid] = topicInfo.user; + if (!topicInfo.teaser) { + return next(null, null); + } + if (!isTopicVisible(topicData, topicInfo)) { return next(null, null); } diff --git a/src/user.js b/src/user.js index 0be4ae124c..ba55215512 100644 --- a/src/user.js +++ b/src/user.js @@ -281,7 +281,7 @@ var bcrypt = require('bcryptjs'), User.isAdministrator(user.uid, next); }, function(isAdmin, next) { - user.status = !user.status ? 'online' : ''; + user.status = !user.status ? 'online' : user.status; user.administrator = isAdmin ? '1':'0'; if (set === 'users:online') { return callback(null, user); diff --git a/src/user/admin.js b/src/user/admin.js index ece5b56cf3..b9f3350135 100644 --- a/src/user/admin.js +++ b/src/user/admin.js @@ -2,7 +2,15 @@ 'use strict'; var async = require('async'), - db = require('./../database'); + db = require('./../database'), + posts = require('./../posts'), + user = require('./../user'), + topics = require('./../topics'), + categories = require('./../categories'), + plugins = require('./../plugins'), + events = require('./../events'), + groups = require('./../groups'); + module.exports = function(User) { @@ -51,4 +59,320 @@ module.exports = function(User) { User.unban = function(uid, callback) { User.setUserField(uid, 'banned', 0, callback); }; -}; \ No newline at end of file + + User.delete = function(adminUid, uid, callback) { + async.waterfall([ + function(next) { + deletePosts(uid, next); + }, + function(next) { + deleteTopics(uid, next); + }, + function(next) { + events.logAdminUserDelete(adminUid, uid, next); + } + ], function(err) { + if (err) { + return callback(err); + } + + deleteAccount(uid, callback); + }); + }; + + function deletePosts(uid, callback) { + db.getSortedSetRange('uid:' + uid + ':posts', 0, -1, function(err, pids) { + if (err) { + return callback(err); + } + + async.each(pids, deletePost, callback); + }); + } + + function deletePost(pid, callback) { + async.parallel([ + function(next) { + deletePostFromTopic(pid, next); + }, + function(next) { + deletePostFromCategoryRecentPosts(pid, next); + }, + function(next) { + deletePostFromUsersFavourites(pid, next); + }, + function(next) { + deletePostFromUsersVotes(pid, next); + }, + function(next) { + db.sortedSetRemove('posts:pid', pid, next); + } + ], function(err) { + if (err) { + return callback(err); + } + + plugins.fireHook('action:post.delete', pid); + db.delete('post:' + pid, callback); + }); + } + + function deletePostFromTopic(pid, callback) { + posts.getPostFields(pid, ['tid', 'deleted'], function(err, postData) { + if (err) { + return callback(err); + } + + db.sortedSetRemove('tid:' + postData.tid + ':posts', pid, function(err) { + if (err) { + return callback(err); + } + + if (parseInt(postData.deleted, 10) === 0) { + db.decrObjectField('global', 'postCount', callback); + } else { + callback(); + } + }); + }); + } + + function deletePostFromCategoryRecentPosts(pid, callback) { + db.getSortedSetRange('categories:cid', 0, -1, function(err, cids) { + if (err) { + return callback(err); + } + + async.each(cids, function(cid, next) { + db.sortedSetRemove('categories:recent_posts:cid:' + cid, pid, next); + }, callback); + }); + } + + function deletePostFromUsersFavourites(pid, callback) { + db.getSetMembers('pid:' + pid + ':users_favourited', function(err, uids) { + if (err) { + return callback(err); + } + + async.each(uids, function(uid, next) { + db.sortedSetRemove('uid:' + uid + ':favourites', pid, next); + }, function(err) { + if (err) { + return callback(err); + } + + db.delete('pid:' + pid + ':users_favourited', callback); + }); + }); + } + + function deletePostFromUsersVotes(pid, callback) { + async.parallel({ + upvoters: function(next) { + db.getSetMembers('pid:' + pid + ':upvote', next); + }, + downvoters: function(next) { + db.getSetMembers('pid:' + pid + ':downvote', next); + } + }, function(err, results) { + if (err) { + return callback(err); + } + + async.parallel([ + function(next) { + async.each(results.upvoters, function(uid, next) { + db.sortedSetRemove('uid:' + uid + ':upvote', pid, next); + }, next); + }, + function(next) { + async.each(results.downvoters, function(uid, next) { + db.sortedSetRemove('uid:' + uid + ':downvote', pid, next); + }, next); + } + ], callback); + }); + } + + function deleteTopics(uid, callback) { + db.getSortedSetRange('uid:' + uid + ':topics', 0, -1, function(err, tids) { + if (err) { + return callback(err); + } + async.each(tids, deleteTopic, callback); + }); + } + + function deleteTopic(tid, callback) { + + async.parallel([ + function(next) { + db.delete('tid:' + tid + ':followers', next); + }, + function(next) { + db.delete('tid:' + tid + ':read_by_uid', next); + }, + function(next) { + db.sortedSetRemove('topics:tid', tid, next); + }, + function(next) { + db.sortedSetRemove('topics:recent', tid, next); + }, + function(next) { + db.sortedSetRemove('topics:posts', tid, next); + }, + function(next) { + db.sortedSetRemove('topics:views', tid, next); + }, + function(next) { + deleteTopicFromCategory(tid, next); + } + ], function(err) { + if (err) { + return callback(err); + } + plugins.fireHook('action:topic.delete', tid); + db.delete('topic:' + tid, callback); + }); + } + + function deleteTopicFromCategory(tid, callback) { + topics.getTopicFields(tid, ['cid', 'deleted'], function(err, topicData) { + if (err) { + return callback(err); + } + + db.sortedSetRemove('categories:' + topicData.cid + ':tid', tid, function(err) { + if (err) { + return callback(err); + } + + db.decrObjectField('category:' + topicData.cid, 'topic_count', function(err) { + if (err) { + return callback(err); + } + + if (parseInt(topicData.deleted) === 0) { + db.decrObjectField('global', 'topicCount', callback); + } else { + callback(); + } + }); + }); + }); + } + + function deleteAccount(uid, callback) { + user.getUserFields(uid, ['username', 'userslug', 'email'], function(err, userData) { + if (err) { + return callback(err); + } + + async.parallel([ + function(next) { + db.deleteObjectField('username:uid', userData.username, next); + }, + function(next) { + db.deleteObjectField('userslug:uid', userData.userslug, next); + }, + function(next) { + db.deleteObjectField('email:uid', userData.email, next); + }, + function(next) { + db.delete('uid:' + uid + ':notifications:read', next); + }, + function(next) { + db.delete('uid:' + uid + ':notifications:unread', next); + }, + function(next) { + db.sortedSetRemove('users:joindate', uid, next); + }, + function(next) { + db.sortedSetRemove('users:postcount', uid, next); + }, + function(next) { + db.sortedSetRemove('users:reputation', uid, next); + }, + function(next) { + db.delete('uid:' + uid + ':favourites', next); + }, + function(next) { + db.delete('uid:' + uid + ':topics', next); + }, + function(next) { + db.delete('uid:' + uid + ':posts', next); + }, + function(next) { + db.delete('uid:' + uid + ':chats', next); + }, + function(next) { + db.delete('uid:' + uid + ':ip', next); + }, + function(next) { + db.delete('uid:' + uid + ':upvote', next); + }, + function(next) { + db.delete('uid:' + uid + ':downvote', next); + }, + function(next) { + deleteUserFromCategoryActiveUsers(uid, next); + }, + function(next) { + deleteUserFromFollowers(uid, next); + }, + function(next) { + deleteUserFromGroups(uid, next); + } + ], function(err) { + if (err) { + return callback(err); + } + + async.parallel([ + function(next) { + db.delete('followers:' + uid, next); + }, + function(next) { + db.delete('following:' + uid, next); + }, + function(next) { + db.delete('user:' + uid, next); + } + ], callback); + }); + }); + } + + function deleteUserFromCategoryActiveUsers(uid, callback) { + db.getSortedSetRange('categories:cid', 0, -1, function(err, cids) { + if (err) { + return callback(err); + } + + async.each(cids, function(cid, next) { + categories.removeActiveUser(cid, uid, next); + }, callback); + }); + } + + function deleteUserFromFollowers(uid, callback) { + db.getSetMembers('followers:' + uid, function(err, uids) { + if (err) { + return callback(err); + } + + async.each(uids, function(theiruid, next) { + db.setRemove('following:' + theiruid, uid, next); + }, callback); + }); + } + + function deleteUserFromGroups(uid, callback) { + groups.getGroupIds(function(err, gids) { + async.each(gids, function(gid, next) { + groups.leave(gid, uid, next); + }, callback); + }); + } +};