diff --git a/install/package.json b/install/package.json index 69c030ce2f..9846de45ce 100644 --- a/install/package.json +++ b/install/package.json @@ -103,10 +103,10 @@ "nodebb-plugin-ntfy": "1.7.4", "nodebb-plugin-spam-be-gone": "2.2.2", "nodebb-rewards-essentials": "1.0.0", - "nodebb-theme-harmony": "1.2.50", + "nodebb-theme-harmony": "1.2.51", "nodebb-theme-lavender": "7.1.8", "nodebb-theme-peace": "2.2.4", - "nodebb-theme-persona": "13.3.15", + "nodebb-theme-persona": "13.3.16", "nodebb-widget-essentials": "7.0.15", "nodemailer": "6.9.13", "nprogress": "0.2.0", diff --git a/public/language/en-GB/user.json b/public/language/en-GB/user.json index 1b338268bf..cd9b17cd0a 100644 --- a/public/language/en-GB/user.json +++ b/public/language/en-GB/user.json @@ -1,7 +1,9 @@ { "user-menu": "User menu", "banned": "Banned", + "unbanned": "Unbanned", "muted": "Muted", + "unmuted": "Unmuted", "offline": "Offline", "deleted": "Deleted", "username": "User Name", @@ -184,6 +186,7 @@ "info.no-ban-history": "This user has never been banned", "info.banned-until": "Banned until %1", "info.banned-expiry": "Expiry", + "info.ban-expired": "Ban expired", "info.banned-permanently": "Banned permanently", "info.banned-reason-label": "Reason", "info.banned-no-reason": "No reason given.", diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index 740a8478dc..0f8dffaa29 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -259,11 +259,11 @@ define('admin/manage/users', [ } Benchpress.render('modals/temporary-ban', {}).then(function (html) { - bootbox.dialog({ - className: 'ban-modal', + const modal = bootbox.dialog({ title: '[[user:ban-account]]', message: html, show: true, + onEscape: true, buttons: { close: { label: '[[global:close]]', @@ -272,7 +272,7 @@ define('admin/manage/users', [ submit: { label: '[[admin/manage/users:alerts.button-ban-x, ' + uids.length + ']]', callback: function () { - const formData = $('.ban-modal form').serializeArray().reduce(function (data, cur) { + const formData = modal.find('form').serializeArray().reduce(function (data, cur) { data[cur.name] = cur.value; return data; }, {}); @@ -302,10 +302,37 @@ define('admin/manage/users', [ return false; // specifically to keep the menu open } - Promise.all(uids.map(function (uid) { - return api.del('/users/' + uid + '/ban'); - })).then(() => { - onSuccess('[[admin/manage/users:alerts.unban-success]]', '.ban', false); + Benchpress.render('modals/unban', {}).then(function (html) { + const modal = bootbox.dialog({ + title: '[[user:unban-account]]', + message: html, + show: true, + onEscape: true, + buttons: { + close: { + label: '[[global:close]]', + className: 'btn-link', + }, + submit: { + label: '[[user:unban-account]]', + callback: function () { + const formData = modal.find('form').serializeArray().reduce(function (data, cur) { + data[cur.name] = cur.value; + return data; + }, {}); + + + Promise.all(uids.map(function (uid) { + return api.del('/users/' + uid + '/ban', { + reason: formData.reason || '', + }); + })).then(() => { + onSuccess('[[admin/manage/users:alerts.unban-success]]', '.ban', false); + }).catch(alerts.error); + }, + }, + }, + }); }); }); diff --git a/public/src/modules/accounts/moderate.js b/public/src/modules/accounts/moderate.js index 3a230f098a..9b1308a8e1 100644 --- a/public/src/modules/accounts/moderate.js +++ b/public/src/modules/accounts/moderate.js @@ -11,98 +11,105 @@ define('forum/account/moderate', [ AccountModerate.banAccount = function (theirid, onSuccess) { theirid = theirid || ajaxify.data.theirid; - Benchpress.render('modals/temporary-ban', {}).then(function (html) { - bootbox.dialog({ - className: 'ban-modal', - title: '[[user:ban-account]]', - message: html, - show: true, - buttons: { - close: { - label: '[[global:close]]', - className: 'btn-link', - }, - submit: { - label: '[[user:ban-account]]', - callback: function () { - const formData = $('.ban-modal form').serializeArray().reduce(function (data, cur) { - data[cur.name] = cur.value; - return data; - }, {}); + throwModal({ + tpl: 'modals/temporary-ban', + title: '[[user:ban-account]]', + onSubmit: function (formData) { + const until = formData.length > 0 ? ( + Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1)) + ) : 0; + api.put('/users/' + theirid + '/ban', { + until: until, + reason: formData.reason || '', + }).then(() => { + if (typeof onSuccess === 'function') { + return onSuccess(); + } - const until = formData.length > 0 ? ( - Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1)) - ) : 0; - - api.put('/users/' + theirid + '/ban', { - until: until, - reason: formData.reason || '', - }).then(() => { - if (typeof onSuccess === 'function') { - return onSuccess(); - } - - ajaxify.refresh(); - }).catch(alerts.error); - }, - }, - }, - }); + ajaxify.refresh(); + }).catch(alerts.error); + }, }); }; AccountModerate.unbanAccount = function (theirid) { - api.del('/users/' + theirid + '/ban').then(() => { - ajaxify.refresh(); - }).catch(alerts.error); + throwModal({ + tpl: 'modals/unban', + title: '[[user:unban-account]]', + onSubmit: function (formData) { + api.del('/users/' + theirid + '/ban', { + reason: formData.reason || '', + }).then(() => { + ajaxify.refresh(); + }).catch(alerts.error); + }, + }); }; AccountModerate.muteAccount = function (theirid, onSuccess) { theirid = theirid || ajaxify.data.theirid; - Benchpress.render('modals/temporary-mute', {}).then(function (html) { - bootbox.dialog({ - className: 'mute-modal', - title: '[[user:mute-account]]', + throwModal({ + tpl: 'modals/temporary-mute', + title: '[[user:mute-account]]', + onSubmit: function (formData) { + const until = formData.length > 0 ? ( + Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1)) + ) : 0; + + api.put('/users/' + theirid + '/mute', { + until: until, + reason: formData.reason || '', + }).then(() => { + if (typeof onSuccess === 'function') { + return onSuccess(); + } + ajaxify.refresh(); + }).catch(alerts.error); + }, + }); + }; + + AccountModerate.unmuteAccount = function (theirid) { + throwModal({ + tpl: 'modals/unmute', + title: '[[user:unmute-account]]', + onSubmit: function (formData) { + api.del('/users/' + theirid + '/mute', { + reason: formData.reason || '', + }).then(() => { + ajaxify.refresh(); + }).catch(alerts.error); + }, + }); + }; + + function throwModal(options) { + Benchpress.render(options.tpl, {}).then(function (html) { + const modal = bootbox.dialog({ + title: options.title, message: html, show: true, + onEscape: true, buttons: { close: { label: '[[global:close]]', className: 'btn-link', }, submit: { - label: '[[user:mute-account]]', + label: options.title, callback: function () { - const formData = $('.mute-modal form').serializeArray().reduce(function (data, cur) { + const formData = modal.find('form').serializeArray().reduce(function (data, cur) { data[cur.name] = cur.value; return data; }, {}); - const until = formData.length > 0 ? ( - Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1)) - ) : 0; - - api.put('/users/' + theirid + '/mute', { - until: until, - reason: formData.reason || '', - }).then(() => { - if (typeof onSuccess === 'function') { - return onSuccess(); - } - ajaxify.refresh(); - }).catch(alerts.error); + options.onSubmit(formData); }, }, }, }); }); - }; - - AccountModerate.unmuteAccount = function (theirid) { - api.del('/users/' + theirid + '/mute').then(() => { - ajaxify.refresh(); - }).catch(alerts.error); - }; + } return AccountModerate; }); diff --git a/src/api/users.js b/src/api/users.js index b2fcb7b38e..1f44bb4372 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -252,7 +252,8 @@ usersAPI.unban = async function (caller, data) { throw new Error('[[error:no-privileges]]'); } - await user.bans.unban(data.uid); + const unbanData = await user.bans.unban(data.uid, data.reason); + await db.setObjectField(`uid:${data.uid}:unban:${unbanData.timestamp}`, 'fromUid', caller.uid); sockets.in(`uid_${data.uid}`).emit('event:unbanned'); @@ -283,6 +284,7 @@ usersAPI.mute = async function (caller, data) { const now = Date.now(); const muteKey = `uid:${data.uid}:mute:${now}`; const muteData = { + type: 'mute', fromUid: caller.uid, uid: data.uid, timestamp: now, @@ -315,7 +317,19 @@ usersAPI.unmute = async function (caller, data) { } await db.deleteObjectFields(`user:${data.uid}`, ['mutedUntil', 'mutedReason']); - + const now = Date.now(); + const unmuteKey = `uid:${data.uid}:unmute:${now}`; + const unmuteData = { + type: 'unmute', + fromUid: caller.uid, + uid: data.uid, + timestamp: now, + }; + if (data.reason) { + unmuteData.reason = data.reason; + } + await db.sortedSetAdd(`uid:${data.uid}:unmutes:timestamp`, now, unmuteKey); + await db.setObject(unmuteKey, unmuteData); await events.log({ type: 'user-unmute', uid: caller.uid, diff --git a/src/user/bans.js b/src/user/bans.js index 8a7352d29e..465f6300e6 100644 --- a/src/user/bans.js +++ b/src/user/bans.js @@ -26,6 +26,7 @@ module.exports = function (User) { const banKey = `uid:${uid}:ban:${now}`; const banData = { + type: 'ban', uid: uid, timestamp: now, expire: until > now ? until : 0, @@ -63,24 +64,39 @@ module.exports = function (User) { return banData; }; - User.bans.unban = async function (uids) { - uids = Array.isArray(uids) ? uids : [uids]; + User.bans.unban = async function (uids, reason = '') { + const isArray = Array.isArray(uids); + uids = isArray ? uids : [uids]; const userData = await User.getUsersFields(uids, ['email:confirmed']); await db.setObject(uids.map(uid => `user:${uid}`), { 'banned:expire': 0 }); - + const now = Date.now(); + const unbanDataArray = []; /* eslint-disable no-await-in-loop */ for (const user of userData) { const systemGroupsToJoin = [ 'registered-users', (parseInt(user['email:confirmed'], 10) === 1 ? 'verified-users' : 'unverified-users'), ]; - await groups.leave(groups.BANNED_USERS, user.uid); - // An unbanned user would lost its previous "Global Moderator" status - await groups.join(systemGroupsToJoin, user.uid); + const unbanKey = `uid:${user.uid}:unban:${now}`; + const unbanData = { + type: 'unban', + uid: user.uid, + reason, + timestamp: now, + }; + await Promise.all([ + db.sortedSetAdd(`uid:${user.uid}:unbans:timestamp`, now, unbanKey), + db.setObject(unbanKey, unbanData), + groups.leave(groups.BANNED_USERS, user.uid), + // An unbanned user would lost its previous "Global Moderator" status + groups.join(systemGroupsToJoin, user.uid), + ]); + unbanDataArray.push(unbanData); } await db.sortedSetRemove(['users:banned', 'users:banned:expire'], uids); + return isArray ? unbanDataArray : unbanDataArray[0]; }; User.bans.isBanned = async function (uids) { diff --git a/src/user/data.js b/src/user/data.js index e21cf92921..c7e2d8b828 100644 --- a/src/user/data.js +++ b/src/user/data.js @@ -258,7 +258,7 @@ module.exports = function (User) { user.banned_until = unban ? 0 : user['banned:expire']; user.banned_until_readable = user.banned_until && !unban ? utils.toISOString(user.banned_until) : 'Not Banned'; if (unban) { - await User.bans.unban(user.uid); + await User.bans.unban(user.uid, '[[user:info.ban-expired]]'); user.banned = false; } } diff --git a/src/user/info.js b/src/user/info.js index d4667bd83f..1c62f3f875 100644 --- a/src/user/info.js +++ b/src/user/info.js @@ -32,8 +32,12 @@ module.exports = function (User) { User.getModerationHistory = async function (uid) { let [flags, bans, mutes] = await Promise.all([ db.getSortedSetRevRangeWithScores(`flags:byTargetUid:${uid}`, 0, 19), - db.getSortedSetRevRange(`uid:${uid}:bans:timestamp`, 0, 19), - db.getSortedSetRevRange(`uid:${uid}:mutes:timestamp`, 0, 19), + db.getSortedSetRevRange([ + `uid:${uid}:bans:timestamp`, `uid:${uid}:unbans:timestamp`, + ], 0, 19), + db.getSortedSetRevRange([ + `uid:${uid}:mutes:timestamp`, `uid:${uid}:unmutes:timestamp`, + ], 0, 19), ]); // Get pids from flag objects diff --git a/src/views/modals/unban.tpl b/src/views/modals/unban.tpl new file mode 100644 index 0000000000..4a3ab69896 --- /dev/null +++ b/src/views/modals/unban.tpl @@ -0,0 +1,10 @@ +
+
+
+
+ + +
+
+
+
diff --git a/src/views/modals/unmute.tpl b/src/views/modals/unmute.tpl new file mode 100644 index 0000000000..4a3ab69896 --- /dev/null +++ b/src/views/modals/unmute.tpl @@ -0,0 +1,10 @@ +
+
+
+
+ + +
+
+
+