mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-27 09:06:15 +01:00
@@ -103,10 +103,10 @@
|
|||||||
"nodebb-plugin-ntfy": "1.7.4",
|
"nodebb-plugin-ntfy": "1.7.4",
|
||||||
"nodebb-plugin-spam-be-gone": "2.2.2",
|
"nodebb-plugin-spam-be-gone": "2.2.2",
|
||||||
"nodebb-rewards-essentials": "1.0.0",
|
"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-lavender": "7.1.8",
|
||||||
"nodebb-theme-peace": "2.2.4",
|
"nodebb-theme-peace": "2.2.4",
|
||||||
"nodebb-theme-persona": "13.3.15",
|
"nodebb-theme-persona": "13.3.16",
|
||||||
"nodebb-widget-essentials": "7.0.15",
|
"nodebb-widget-essentials": "7.0.15",
|
||||||
"nodemailer": "6.9.13",
|
"nodemailer": "6.9.13",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"user-menu": "User menu",
|
"user-menu": "User menu",
|
||||||
"banned": "Banned",
|
"banned": "Banned",
|
||||||
|
"unbanned": "Unbanned",
|
||||||
"muted": "Muted",
|
"muted": "Muted",
|
||||||
|
"unmuted": "Unmuted",
|
||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
"deleted": "Deleted",
|
"deleted": "Deleted",
|
||||||
"username": "User Name",
|
"username": "User Name",
|
||||||
@@ -184,6 +186,7 @@
|
|||||||
"info.no-ban-history": "This user has never been banned",
|
"info.no-ban-history": "This user has never been banned",
|
||||||
"info.banned-until": "Banned until %1",
|
"info.banned-until": "Banned until %1",
|
||||||
"info.banned-expiry": "Expiry",
|
"info.banned-expiry": "Expiry",
|
||||||
|
"info.ban-expired": "Ban expired",
|
||||||
"info.banned-permanently": "Banned permanently",
|
"info.banned-permanently": "Banned permanently",
|
||||||
"info.banned-reason-label": "Reason",
|
"info.banned-reason-label": "Reason",
|
||||||
"info.banned-no-reason": "No reason given.",
|
"info.banned-no-reason": "No reason given.",
|
||||||
|
|||||||
@@ -259,11 +259,11 @@ define('admin/manage/users', [
|
|||||||
}
|
}
|
||||||
|
|
||||||
Benchpress.render('modals/temporary-ban', {}).then(function (html) {
|
Benchpress.render('modals/temporary-ban', {}).then(function (html) {
|
||||||
bootbox.dialog({
|
const modal = bootbox.dialog({
|
||||||
className: 'ban-modal',
|
|
||||||
title: '[[user:ban-account]]',
|
title: '[[user:ban-account]]',
|
||||||
message: html,
|
message: html,
|
||||||
show: true,
|
show: true,
|
||||||
|
onEscape: true,
|
||||||
buttons: {
|
buttons: {
|
||||||
close: {
|
close: {
|
||||||
label: '[[global:close]]',
|
label: '[[global:close]]',
|
||||||
@@ -272,7 +272,7 @@ define('admin/manage/users', [
|
|||||||
submit: {
|
submit: {
|
||||||
label: '[[admin/manage/users:alerts.button-ban-x, ' + uids.length + ']]',
|
label: '[[admin/manage/users:alerts.button-ban-x, ' + uids.length + ']]',
|
||||||
callback: function () {
|
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;
|
data[cur.name] = cur.value;
|
||||||
return data;
|
return data;
|
||||||
}, {});
|
}, {});
|
||||||
@@ -302,10 +302,37 @@ define('admin/manage/users', [
|
|||||||
return false; // specifically to keep the menu open
|
return false; // specifically to keep the menu open
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all(uids.map(function (uid) {
|
Benchpress.render('modals/unban', {}).then(function (html) {
|
||||||
return api.del('/users/' + uid + '/ban');
|
const modal = bootbox.dialog({
|
||||||
})).then(() => {
|
title: '[[user:unban-account]]',
|
||||||
onSuccess('[[admin/manage/users:alerts.unban-success]]', '.ban', false);
|
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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,98 +11,105 @@ define('forum/account/moderate', [
|
|||||||
AccountModerate.banAccount = function (theirid, onSuccess) {
|
AccountModerate.banAccount = function (theirid, onSuccess) {
|
||||||
theirid = theirid || ajaxify.data.theirid;
|
theirid = theirid || ajaxify.data.theirid;
|
||||||
|
|
||||||
Benchpress.render('modals/temporary-ban', {}).then(function (html) {
|
throwModal({
|
||||||
bootbox.dialog({
|
tpl: 'modals/temporary-ban',
|
||||||
className: 'ban-modal',
|
title: '[[user:ban-account]]',
|
||||||
title: '[[user:ban-account]]',
|
onSubmit: function (formData) {
|
||||||
message: html,
|
const until = formData.length > 0 ? (
|
||||||
show: true,
|
Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1))
|
||||||
buttons: {
|
) : 0;
|
||||||
close: {
|
api.put('/users/' + theirid + '/ban', {
|
||||||
label: '[[global:close]]',
|
until: until,
|
||||||
className: 'btn-link',
|
reason: formData.reason || '',
|
||||||
},
|
}).then(() => {
|
||||||
submit: {
|
if (typeof onSuccess === 'function') {
|
||||||
label: '[[user:ban-account]]',
|
return onSuccess();
|
||||||
callback: function () {
|
}
|
||||||
const formData = $('.ban-modal form').serializeArray().reduce(function (data, cur) {
|
|
||||||
data[cur.name] = cur.value;
|
|
||||||
return data;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const until = formData.length > 0 ? (
|
ajaxify.refresh();
|
||||||
Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1))
|
}).catch(alerts.error);
|
||||||
) : 0;
|
},
|
||||||
|
|
||||||
api.put('/users/' + theirid + '/ban', {
|
|
||||||
until: until,
|
|
||||||
reason: formData.reason || '',
|
|
||||||
}).then(() => {
|
|
||||||
if (typeof onSuccess === 'function') {
|
|
||||||
return onSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
ajaxify.refresh();
|
|
||||||
}).catch(alerts.error);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
AccountModerate.unbanAccount = function (theirid) {
|
AccountModerate.unbanAccount = function (theirid) {
|
||||||
api.del('/users/' + theirid + '/ban').then(() => {
|
throwModal({
|
||||||
ajaxify.refresh();
|
tpl: 'modals/unban',
|
||||||
}).catch(alerts.error);
|
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) {
|
AccountModerate.muteAccount = function (theirid, onSuccess) {
|
||||||
theirid = theirid || ajaxify.data.theirid;
|
theirid = theirid || ajaxify.data.theirid;
|
||||||
Benchpress.render('modals/temporary-mute', {}).then(function (html) {
|
throwModal({
|
||||||
bootbox.dialog({
|
tpl: 'modals/temporary-mute',
|
||||||
className: 'mute-modal',
|
title: '[[user:mute-account]]',
|
||||||
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,
|
message: html,
|
||||||
show: true,
|
show: true,
|
||||||
|
onEscape: true,
|
||||||
buttons: {
|
buttons: {
|
||||||
close: {
|
close: {
|
||||||
label: '[[global:close]]',
|
label: '[[global:close]]',
|
||||||
className: 'btn-link',
|
className: 'btn-link',
|
||||||
},
|
},
|
||||||
submit: {
|
submit: {
|
||||||
label: '[[user:mute-account]]',
|
label: options.title,
|
||||||
callback: function () {
|
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;
|
data[cur.name] = cur.value;
|
||||||
return data;
|
return data;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const until = formData.length > 0 ? (
|
options.onSubmit(formData);
|
||||||
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) {
|
|
||||||
api.del('/users/' + theirid + '/mute').then(() => {
|
|
||||||
ajaxify.refresh();
|
|
||||||
}).catch(alerts.error);
|
|
||||||
};
|
|
||||||
|
|
||||||
return AccountModerate;
|
return AccountModerate;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -252,7 +252,8 @@ usersAPI.unban = async function (caller, data) {
|
|||||||
throw new Error('[[error:no-privileges]]');
|
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');
|
sockets.in(`uid_${data.uid}`).emit('event:unbanned');
|
||||||
|
|
||||||
@@ -283,6 +284,7 @@ usersAPI.mute = async function (caller, data) {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const muteKey = `uid:${data.uid}:mute:${now}`;
|
const muteKey = `uid:${data.uid}:mute:${now}`;
|
||||||
const muteData = {
|
const muteData = {
|
||||||
|
type: 'mute',
|
||||||
fromUid: caller.uid,
|
fromUid: caller.uid,
|
||||||
uid: data.uid,
|
uid: data.uid,
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
@@ -315,7 +317,19 @@ usersAPI.unmute = async function (caller, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db.deleteObjectFields(`user:${data.uid}`, ['mutedUntil', 'mutedReason']);
|
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({
|
await events.log({
|
||||||
type: 'user-unmute',
|
type: 'user-unmute',
|
||||||
uid: caller.uid,
|
uid: caller.uid,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ module.exports = function (User) {
|
|||||||
|
|
||||||
const banKey = `uid:${uid}:ban:${now}`;
|
const banKey = `uid:${uid}:ban:${now}`;
|
||||||
const banData = {
|
const banData = {
|
||||||
|
type: 'ban',
|
||||||
uid: uid,
|
uid: uid,
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
expire: until > now ? until : 0,
|
expire: until > now ? until : 0,
|
||||||
@@ -63,24 +64,39 @@ module.exports = function (User) {
|
|||||||
return banData;
|
return banData;
|
||||||
};
|
};
|
||||||
|
|
||||||
User.bans.unban = async function (uids) {
|
User.bans.unban = async function (uids, reason = '') {
|
||||||
uids = Array.isArray(uids) ? uids : [uids];
|
const isArray = Array.isArray(uids);
|
||||||
|
uids = isArray ? uids : [uids];
|
||||||
const userData = await User.getUsersFields(uids, ['email:confirmed']);
|
const userData = await User.getUsersFields(uids, ['email:confirmed']);
|
||||||
|
|
||||||
await db.setObject(uids.map(uid => `user:${uid}`), { 'banned:expire': 0 });
|
await db.setObject(uids.map(uid => `user:${uid}`), { 'banned:expire': 0 });
|
||||||
|
const now = Date.now();
|
||||||
|
const unbanDataArray = [];
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
for (const user of userData) {
|
for (const user of userData) {
|
||||||
const systemGroupsToJoin = [
|
const systemGroupsToJoin = [
|
||||||
'registered-users',
|
'registered-users',
|
||||||
(parseInt(user['email:confirmed'], 10) === 1 ? 'verified-users' : 'unverified-users'),
|
(parseInt(user['email:confirmed'], 10) === 1 ? 'verified-users' : 'unverified-users'),
|
||||||
];
|
];
|
||||||
await groups.leave(groups.BANNED_USERS, user.uid);
|
const unbanKey = `uid:${user.uid}:unban:${now}`;
|
||||||
// An unbanned user would lost its previous "Global Moderator" status
|
const unbanData = {
|
||||||
await groups.join(systemGroupsToJoin, user.uid);
|
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);
|
await db.sortedSetRemove(['users:banned', 'users:banned:expire'], uids);
|
||||||
|
return isArray ? unbanDataArray : unbanDataArray[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
User.bans.isBanned = async function (uids) {
|
User.bans.isBanned = async function (uids) {
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ module.exports = function (User) {
|
|||||||
user.banned_until = unban ? 0 : user['banned:expire'];
|
user.banned_until = unban ? 0 : user['banned:expire'];
|
||||||
user.banned_until_readable = user.banned_until && !unban ? utils.toISOString(user.banned_until) : 'Not Banned';
|
user.banned_until_readable = user.banned_until && !unban ? utils.toISOString(user.banned_until) : 'Not Banned';
|
||||||
if (unban) {
|
if (unban) {
|
||||||
await User.bans.unban(user.uid);
|
await User.bans.unban(user.uid, '[[user:info.ban-expired]]');
|
||||||
user.banned = false;
|
user.banned = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,12 @@ module.exports = function (User) {
|
|||||||
User.getModerationHistory = async function (uid) {
|
User.getModerationHistory = async function (uid) {
|
||||||
let [flags, bans, mutes] = await Promise.all([
|
let [flags, bans, mutes] = await Promise.all([
|
||||||
db.getSortedSetRevRangeWithScores(`flags:byTargetUid:${uid}`, 0, 19),
|
db.getSortedSetRevRangeWithScores(`flags:byTargetUid:${uid}`, 0, 19),
|
||||||
db.getSortedSetRevRange(`uid:${uid}:bans:timestamp`, 0, 19),
|
db.getSortedSetRevRange([
|
||||||
db.getSortedSetRevRange(`uid:${uid}:mutes:timestamp`, 0, 19),
|
`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
|
// Get pids from flag objects
|
||||||
|
|||||||
10
src/views/modals/unban.tpl
Normal file
10
src/views/modals/unban.tpl
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<form class="form">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="reason">[[admin/manage/users:temp-ban.reason]]</label>
|
||||||
|
<input type="text" class="form-control" id="reason" name="reason" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
10
src/views/modals/unmute.tpl
Normal file
10
src/views/modals/unmute.tpl
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<form class="form">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="reason">[[admin/manage/users:temp-ban.reason]]</label>
|
||||||
|
<input type="text" class="form-control" id="reason" name="reason" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
Reference in New Issue
Block a user