mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-03-03 19:11:22 +01:00
feat: ban/mute reasons (#13960)
add acp page to create reasons add dropdown to insert them into reason change reason field into textarea translate and parse reason before sending ban email
This commit is contained in:
9
public/language/en-GB/admin/manage/ban-reasons.json
Normal file
9
public/language/en-GB/admin/manage/ban-reasons.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"title": "Manage Ban Reasons",
|
||||||
|
"create-reason": "Create Reason",
|
||||||
|
"edit-reason": "Edit Reason",
|
||||||
|
"reason-title": "Title",
|
||||||
|
"reason-body": "Body",
|
||||||
|
"ban-reasons-saved": "Ban reasons saved successfully",
|
||||||
|
"delete-reason-confirm-x": "Are you sure you want to delete the ban reason with the title <strong>%1</strong>?"
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
"purge": "Delete <strong>User(s)</strong> and <strong>Content</strong>",
|
"purge": "Delete <strong>User(s)</strong> and <strong>Content</strong>",
|
||||||
"download-csv": "Download CSV",
|
"download-csv": "Download CSV",
|
||||||
"custom-user-fields": "Custom User Fields",
|
"custom-user-fields": "Custom User Fields",
|
||||||
|
"ban-reasons": "Ban Reasons",
|
||||||
"manage-groups": "Manage Groups",
|
"manage-groups": "Manage Groups",
|
||||||
"set-reputation": "Set Reputation",
|
"set-reputation": "Set Reputation",
|
||||||
"add-group": "Add Group",
|
"add-group": "Add Group",
|
||||||
@@ -77,9 +78,11 @@
|
|||||||
|
|
||||||
"temp-ban.length": "Length",
|
"temp-ban.length": "Length",
|
||||||
"temp-ban.reason": "Reason <span class=\"text-muted\">(Optional)</span>",
|
"temp-ban.reason": "Reason <span class=\"text-muted\">(Optional)</span>",
|
||||||
|
"temp-ban.select-reason": "Select a reason",
|
||||||
"temp-ban.hours": "Hours",
|
"temp-ban.hours": "Hours",
|
||||||
"temp-ban.days": "Days",
|
"temp-ban.days": "Days",
|
||||||
"temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.",
|
"temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.",
|
||||||
|
"temp-mute.explanation": "Enter the length of time for the mute. Note that a time of 0 will be a considered a permanent mute.",
|
||||||
|
|
||||||
"alerts.confirm-ban": "Do you really want to ban this user <strong>permanently</strong>?",
|
"alerts.confirm-ban": "Do you really want to ban this user <strong>permanently</strong>?",
|
||||||
"alerts.confirm-ban-multi": "Do you really want to ban these users <strong>permanently</strong>?",
|
"alerts.confirm-ban-multi": "Do you really want to ban these users <strong>permanently</strong>?",
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ paths:
|
|||||||
$ref: 'read/admin/manage/users.yaml'
|
$ref: 'read/admin/manage/users.yaml'
|
||||||
/api/admin/manage/users/custom-fields:
|
/api/admin/manage/users/custom-fields:
|
||||||
$ref: 'read/admin/manage/users/custom-fields.yaml'
|
$ref: 'read/admin/manage/users/custom-fields.yaml'
|
||||||
|
/api/admin/manage/users/ban-reasons:
|
||||||
|
$ref: 'read/admin/manage/users/ban-reasons.yaml'
|
||||||
/api/admin/manage/registration:
|
/api/admin/manage/registration:
|
||||||
$ref: 'read/admin/manage/registration.yaml'
|
$ref: 'read/admin/manage/registration.yaml'
|
||||||
/api/admin/manage/admins-mods:
|
/api/admin/manage/admins-mods:
|
||||||
|
|||||||
28
public/openapi/read/admin/manage/users/ban-reasons.yaml
Normal file
28
public/openapi/read/admin/manage/users/ban-reasons.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- admin
|
||||||
|
summary: Manage ban reasons for users
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: ""
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
reasons:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
key:
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
body:
|
||||||
|
type: string
|
||||||
|
parsedBody:
|
||||||
|
type: string
|
||||||
|
description: "body parsed with filter:parse.raw hook"
|
||||||
|
- $ref: ../../../../components/schemas/CommonProps.yaml#/CommonProps
|
||||||
@@ -254,47 +254,52 @@ define('admin/manage/users', [
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.ban-user-temporary').on('click', function () {
|
$('.ban-user-temporary').on('click', async function () {
|
||||||
const uids = getSelectedUids();
|
const uids = getSelectedUids();
|
||||||
if (!uids.length) {
|
if (!uids.length) {
|
||||||
alerts.error('[[error:no-users-selected]]');
|
alerts.error('[[error:no-users-selected]]');
|
||||||
return false; // specifically to keep the menu open
|
return false; // specifically to keep the menu open
|
||||||
}
|
}
|
||||||
|
const reasons = await socket.emit('user.getBanReasons');
|
||||||
|
const html = await app.parseAndTranslate('modals/temporary-ban', { reasons });
|
||||||
|
const modal = bootbox.dialog({
|
||||||
|
title: '[[user:ban-account]]',
|
||||||
|
message: html,
|
||||||
|
show: true,
|
||||||
|
onEscape: true,
|
||||||
|
buttons: {
|
||||||
|
close: {
|
||||||
|
label: '[[global:close]]',
|
||||||
|
className: 'btn-link',
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
label: '[[admin/manage/users:alerts.button-ban-x, ' + uids.length + ']]',
|
||||||
|
callback: function () {
|
||||||
|
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;
|
||||||
|
|
||||||
Benchpress.render('modals/temporary-ban', {}).then(function (html) {
|
Promise.all(uids.map(function (uid) {
|
||||||
const modal = bootbox.dialog({
|
return api.put('/users/' + encodeURIComponent(uid) + '/ban', {
|
||||||
title: '[[user:ban-account]]',
|
until: until,
|
||||||
message: html,
|
reason: formData.reason,
|
||||||
show: true,
|
});
|
||||||
onEscape: true,
|
})).then(() => {
|
||||||
buttons: {
|
onSuccess('[[admin/manage/users:alerts.ban-success]]', '.ban', true);
|
||||||
close: {
|
}).catch(alerts.error);
|
||||||
label: '[[global:close]]',
|
|
||||||
className: 'btn-link',
|
|
||||||
},
|
|
||||||
submit: {
|
|
||||||
label: '[[admin/manage/users:alerts.button-ban-x, ' + uids.length + ']]',
|
|
||||||
callback: function () {
|
|
||||||
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;
|
|
||||||
|
|
||||||
Promise.all(uids.map(function (uid) {
|
|
||||||
return api.put('/users/' + encodeURIComponent(uid) + '/ban', {
|
|
||||||
until: until,
|
|
||||||
reason: formData.reason,
|
|
||||||
});
|
|
||||||
})).then(() => {
|
|
||||||
onSuccess('[[admin/manage/users:alerts.ban-success]]', '.ban', true);
|
|
||||||
}).catch(alerts.error);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
modal.find('[data-key]').on('click', function () {
|
||||||
|
const reason = reasons.find(r => String(r.key) === $(this).attr('data-key'));
|
||||||
|
if (reason && reason.body) {
|
||||||
|
modal.find('[name="reason"]').val(translator.unescape(reason.body));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
96
public/src/admin/manage/users/ban-reasons.js
Normal file
96
public/src/admin/manage/users/ban-reasons.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
define('admin/manage/user/ban-reasons', [
|
||||||
|
'benchpress', 'bootbox', 'alerts', 'translator', 'jquery-ui/widgets/sortable',
|
||||||
|
], function (benchpress, bootbox, alerts, translator) {
|
||||||
|
const manageBanReasons = {};
|
||||||
|
|
||||||
|
manageBanReasons.init = function () {
|
||||||
|
const table = $('table');
|
||||||
|
|
||||||
|
$('#new').on('click', () => showModal());
|
||||||
|
|
||||||
|
table.on('click', '[data-action="edit"]', function () {
|
||||||
|
const row = $(this).parents('[data-key]');
|
||||||
|
showModal(getDataFromEl(row));
|
||||||
|
});
|
||||||
|
|
||||||
|
table.on('click', '[data-action="delete"]', function () {
|
||||||
|
const row = $(this).parents('[data-key]');
|
||||||
|
const title = row.attr('data-title');
|
||||||
|
bootbox.confirm(`[[admin/manage/ban-reasons:delete-reason-confirm-x, "${title}"]]`, function (ok) {
|
||||||
|
if (!ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
row.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('tbody').sortable({
|
||||||
|
handle: '[component="sort/handle"]',
|
||||||
|
axis: 'y',
|
||||||
|
zIndex: 9999,
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#save').on('click', () => {
|
||||||
|
const reasons = [];
|
||||||
|
$('tbody tr[data-key]').each((index, el) => {
|
||||||
|
reasons.push(getDataFromEl($(el)));
|
||||||
|
});
|
||||||
|
socket.emit('admin.user.saveBanReasons', reasons, function (err) {
|
||||||
|
if (err) {
|
||||||
|
return alerts.error(err);
|
||||||
|
}
|
||||||
|
alerts.success('[[admin/manage/ban-reasons:ban-reasons-saved]]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDataFromEl(el) {
|
||||||
|
return {
|
||||||
|
key: el.attr('data-key'),
|
||||||
|
title: el.attr('data-title'),
|
||||||
|
body: el.attr('data-body'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showModal(reason = null) {
|
||||||
|
const html = await benchpress.render('admin/partials/manage-ban-reasons-modal', reason);
|
||||||
|
const modal = bootbox.dialog({
|
||||||
|
message: html,
|
||||||
|
onEscape: true,
|
||||||
|
title: reason ?
|
||||||
|
'[[admin/manage/ban-reasons:edit-reason]]' :
|
||||||
|
'[[admin/manage/ban-reasons:create-reason]]',
|
||||||
|
buttons: {
|
||||||
|
submit: {
|
||||||
|
label: '[[global:save]]',
|
||||||
|
callback: async function () {
|
||||||
|
const formData = modal.find('form').serializeObject();
|
||||||
|
formData.key = reason ? reason.key : Date.now();
|
||||||
|
formData.body = translator.escape(formData.body);
|
||||||
|
formData.parsedBody = translator.escape(await socket.emit('admin.parseRaw', formData.body));
|
||||||
|
|
||||||
|
app.parseAndTranslate('admin/manage/users/ban-reasons', 'reasons', {
|
||||||
|
reasons: [formData],
|
||||||
|
}, (html) => {
|
||||||
|
if (reason) {
|
||||||
|
const oldKey = reason.key;
|
||||||
|
$(`tbody [data-key="${oldKey}"]`).replaceWith(html);
|
||||||
|
} else {
|
||||||
|
$('tbody').append(html);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// bootbox translates message we want the translation keys to be preseved.
|
||||||
|
if (reason && reason.body) {
|
||||||
|
modal.find('[name="body"]').val(reason.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return manageBanReasons;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
define('forum/account/moderate', [
|
define('forum/account/moderate', [
|
||||||
'benchpress',
|
|
||||||
'api',
|
'api',
|
||||||
'bootbox',
|
'bootbox',
|
||||||
'alerts',
|
'alerts',
|
||||||
], function (Benchpress, api, bootbox, alerts) {
|
'translator',
|
||||||
|
], function (api, bootbox, alerts, translator) {
|
||||||
const AccountModerate = {};
|
const AccountModerate = {};
|
||||||
|
|
||||||
AccountModerate.banAccount = function (theirid, onSuccess) {
|
AccountModerate.banAccount = function (theirid, onSuccess) {
|
||||||
@@ -83,31 +83,37 @@ define('forum/account/moderate', [
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function throwModal(options) {
|
async function throwModal(options) {
|
||||||
Benchpress.render(options.tpl, {}).then(function (html) {
|
const reasons = await socket.emit('user.getBanReasons');
|
||||||
const modal = bootbox.dialog({
|
const html = await app.parseAndTranslate(options.tpl, { reasons });
|
||||||
title: options.title,
|
const modal = bootbox.dialog({
|
||||||
message: html,
|
title: options.title,
|
||||||
show: true,
|
message: html,
|
||||||
onEscape: true,
|
show: true,
|
||||||
buttons: {
|
onEscape: true,
|
||||||
close: {
|
buttons: {
|
||||||
label: '[[global:close]]',
|
close: {
|
||||||
className: 'btn-link',
|
label: '[[global:close]]',
|
||||||
},
|
className: 'btn-link',
|
||||||
submit: {
|
},
|
||||||
label: options.title,
|
submit: {
|
||||||
callback: function () {
|
label: options.title,
|
||||||
const formData = modal.find('form').serializeArray().reduce(function (data, cur) {
|
callback: function () {
|
||||||
data[cur.name] = cur.value;
|
const formData = modal.find('form').serializeArray().reduce(function (data, cur) {
|
||||||
return data;
|
data[cur.name] = cur.value;
|
||||||
}, {});
|
return data;
|
||||||
|
}, {});
|
||||||
|
|
||||||
options.onSubmit(formData);
|
options.onSubmit(formData);
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
modal.find('[data-key]').on('click', function () {
|
||||||
|
const reason = reasons.find(r => String(r.key) === $(this).attr('data-key'));
|
||||||
|
if (reason && reason.body) {
|
||||||
|
modal.find('[name="reason"]').val(translator.unescape(reason.body));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -314,3 +314,8 @@ usersController.customFields = async function (req, res) {
|
|||||||
});
|
});
|
||||||
res.render('admin/manage/users/custom-fields', { fields: fields });
|
res.render('admin/manage/users/custom-fields', { fields: fields });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
usersController.banReasons = async function (req, res) {
|
||||||
|
const reasons = await user.bans.getBanReasons();
|
||||||
|
res.render('admin/manage/users/ban-reasons', { reasons });
|
||||||
|
};
|
||||||
@@ -23,6 +23,7 @@ module.exports = function (app, name, middleware, controllers) {
|
|||||||
|
|
||||||
helpers.setupAdminPageRoute(app, `/${name}/manage/users`, middlewares, controllers.admin.users.index);
|
helpers.setupAdminPageRoute(app, `/${name}/manage/users`, middlewares, controllers.admin.users.index);
|
||||||
helpers.setupAdminPageRoute(app, `/${name}/manage/users/custom-fields`, middlewares, controllers.admin.users.customFields);
|
helpers.setupAdminPageRoute(app, `/${name}/manage/users/custom-fields`, middlewares, controllers.admin.users.customFields);
|
||||||
|
helpers.setupAdminPageRoute(app, `/${name}/manage/users/ban-reasons`, middlewares, controllers.admin.users.banReasons);
|
||||||
helpers.setupAdminPageRoute(app, `/${name}/manage/registration`, middlewares, controllers.admin.users.registrationQueue);
|
helpers.setupAdminPageRoute(app, `/${name}/manage/registration`, middlewares, controllers.admin.users.registrationQueue);
|
||||||
|
|
||||||
helpers.setupAdminPageRoute(app, `/${name}/manage/admins-mods`, middlewares, controllers.admin.adminsMods.get);
|
helpers.setupAdminPageRoute(app, `/${name}/manage/admins-mods`, middlewares, controllers.admin.adminsMods.get);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const db = require('../database');
|
|||||||
const privileges = require('../privileges');
|
const privileges = require('../privileges');
|
||||||
const websockets = require('./index');
|
const websockets = require('./index');
|
||||||
const batch = require('../batch');
|
const batch = require('../batch');
|
||||||
|
const plugins = require('../plugins');
|
||||||
const index = require('./index');
|
const index = require('./index');
|
||||||
const getAdminSearchDict = require('../admin/search').getDictionary;
|
const getAdminSearchDict = require('../admin/search').getDictionary;
|
||||||
|
|
||||||
@@ -126,4 +127,8 @@ SocketAdmin.clearSearchHistory = async function () {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
SocketAdmin.parseRaw = async function (socket, text) {
|
||||||
|
return await plugins.hooks.fire('filter:parse.raw', text);
|
||||||
|
};
|
||||||
|
|
||||||
require('../promisify')(SocketAdmin);
|
require('../promisify')(SocketAdmin);
|
||||||
|
|||||||
@@ -219,3 +219,11 @@ User.saveCustomFields = async function (socket, fields) {
|
|||||||
await user.reloadCustomFieldWhitelist();
|
await user.reloadCustomFieldWhitelist();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
User.saveBanReasons = async function (socket, reasons) {
|
||||||
|
const keys = await db.getSortedSetRange('ban-reasons', 0, -1);
|
||||||
|
await db.delete('ban-reasons');
|
||||||
|
await db.deleteAll(keys.map(k => `ban-reason:${k}`));
|
||||||
|
const ids = reasons.map((f, i) => i);
|
||||||
|
await db.sortedSetAdd(`ban-reasons`, ids, ids);
|
||||||
|
await db.setObjectBulk(reasons.map((reason, i) => [`ban-reason:${i}`, reason]));
|
||||||
|
};
|
||||||
|
|||||||
@@ -174,6 +174,14 @@ SocketUser.editModerationNote = async function (socket, data) {
|
|||||||
return await user.getModerationNotesByIds(data.uid, [data.id]);
|
return await user.getModerationNotesByIds(data.uid, [data.id]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
SocketUser.getBanReasons = async function (socket) {
|
||||||
|
const canBan = await privileges.users.hasBanPrivilege(socket.uid);
|
||||||
|
if (!canBan) {
|
||||||
|
throw new Error('[[error:no-privileges]]');
|
||||||
|
}
|
||||||
|
return await user.bans.getBanReasons();
|
||||||
|
};
|
||||||
|
|
||||||
SocketUser.deleteUpload = async function (socket, data) {
|
SocketUser.deleteUpload = async function (socket, data) {
|
||||||
if (!data || !data.name || !data.uid) {
|
if (!data || !data.name || !data.uid) {
|
||||||
throw new Error('[[error:invalid-data]]');
|
throw new Error('[[error:invalid-data]]');
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const emailer = require('../emailer');
|
|||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const groups = require('../groups');
|
const groups = require('../groups');
|
||||||
const privileges = require('../privileges');
|
const privileges = require('../privileges');
|
||||||
|
const plugins = require('../plugins');
|
||||||
|
const translator = require('../translator');
|
||||||
|
|
||||||
module.exports = function (User) {
|
module.exports = function (User) {
|
||||||
User.bans = {};
|
User.bans = {};
|
||||||
@@ -50,20 +52,27 @@ module.exports = function (User) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Email notification of ban
|
// Email notification of ban
|
||||||
const username = await User.getUserField(uid, 'username');
|
const [username, settings] = await Promise.all([
|
||||||
|
User.getUserField(uid, 'username'),
|
||||||
|
User.getSettings(uid),
|
||||||
|
]);
|
||||||
const siteTitle = meta.config.title || 'NodeBB';
|
const siteTitle = meta.config.title || 'NodeBB';
|
||||||
|
|
||||||
const data = {
|
await emailer.send('banned', uid, {
|
||||||
subject: `[[email:banned.subject, ${siteTitle}]]`,
|
subject: `[[email:banned.subject, ${siteTitle}]]`,
|
||||||
username: username,
|
username: username,
|
||||||
until: until ? (new Date(until)).toUTCString().replace(/,/g, '\\,') : false,
|
until: until ? (new Date(until)).toUTCString().replace(/,/g, '\\,') : false,
|
||||||
reason: reason,
|
reason: await parseReason(reason, settings.userLang),
|
||||||
};
|
}).catch(err => winston.error(`[emailer.send] ${err.stack}`));
|
||||||
await emailer.send('banned', uid, data).catch(err => winston.error(`[emailer.send] ${err.stack}`));
|
|
||||||
|
|
||||||
return banData;
|
return banData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function parseReason(reason, lang) {
|
||||||
|
const parsed = await plugins.hooks.fire('filter:parse.raw', reason);
|
||||||
|
return await translator.translate(parsed, lang);
|
||||||
|
}
|
||||||
|
|
||||||
User.bans.unban = async function (uids, reason = '') {
|
User.bans.unban = async function (uids, reason = '') {
|
||||||
const isArray = Array.isArray(uids);
|
const isArray = Array.isArray(uids);
|
||||||
uids = isArray ? uids : [uids];
|
uids = isArray ? uids : [uids];
|
||||||
@@ -155,4 +164,15 @@ module.exports = function (User) {
|
|||||||
const banObj = await db.getObject(keys[0]);
|
const banObj = await db.getObject(keys[0]);
|
||||||
return banObj && banObj.reason ? banObj.reason : '';
|
return banObj && banObj.reason ? banObj.reason : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
User.bans.getBanReasons = async function () {
|
||||||
|
const keys = await db.getSortedSetRange('ban-reasons', 0, -1);
|
||||||
|
const reasons = (await db.getObjects(keys.map(k => `ban-reason:${k}`))).filter(Boolean);
|
||||||
|
await Promise.all(reasons.map(async (reason, i) => {
|
||||||
|
reason.key = i;
|
||||||
|
reason.parsedBody = translator.escape(await plugins.hooks.fire('filter:parse.raw', reason.body || ''));
|
||||||
|
reason.body = translator.escape(reason.body);
|
||||||
|
}));
|
||||||
|
return reasons;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -83,6 +83,8 @@
|
|||||||
<li><a target="_blank" href="#" class="dropdown-item rounded-1 export-csv" role="menuitem">[[admin/manage/users:download-csv]]</a></li>
|
<li><a target="_blank" href="#" class="dropdown-item rounded-1 export-csv" role="menuitem">[[admin/manage/users:download-csv]]</a></li>
|
||||||
<li><a class="dropdown-item rounded-1" href="{relative_path}/admin/manage/users/custom-fields">[[admin/manage/users:custom-user-fields]]</a>
|
<li><a class="dropdown-item rounded-1" href="{relative_path}/admin/manage/users/custom-fields">[[admin/manage/users:custom-user-fields]]</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li><a class="dropdown-item rounded-1" href="{relative_path}/admin/manage/users/ban-reasons">[[admin/manage/users:ban-reasons]]</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
47
src/views/admin/manage/users/ban-reasons.tpl
Normal file
47
src/views/admin/manage/users/ban-reasons.tpl
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<div class="manage-users d-flex flex-column gap-2 px-lg-4 h-100">
|
||||||
|
<div class="d-flex border-bottom py-2 m-0 sticky-top acp-page-main-header align-items-center justify-content-between flex-wrap gap-2">
|
||||||
|
<div class="">
|
||||||
|
<h4 class="fw-bold tracking-tight mb-0">[[admin/manage/ban-reasons:title]]</h4>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-1">
|
||||||
|
<button id="new" class="btn btn-light btn-sm text-nowrap" type="button">
|
||||||
|
<i class="fa fa-fw fa-plus"></i> [[admin/manage/ban-reasons:create-reason]]
|
||||||
|
</button>
|
||||||
|
<button id="save" class="btn btn-primary btn-sm fw-semibold ff-secondary w-100 text-center text-nowrap">[[admin/admin:save-changes]]</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row flex-grow-1">
|
||||||
|
<div class="col-lg-12 d-flex flex-column gap-2">
|
||||||
|
<div class="table-responsive flex-grow-1">
|
||||||
|
<table class="table text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th class="text-muted">[[admin/manage/ban-reasons:reason-title]]</th>
|
||||||
|
<th class="text-muted">[[admin/manage/ban-reasons:reason-body]]</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{{ each reasons }}}
|
||||||
|
<tr data-key="{./key}" data-title="{./title}" data-body="{./body}">
|
||||||
|
<td class="" style="width: 32px;">
|
||||||
|
<a href="#" component="sort/handle" class="btn btn-light btn-sm d-none d-md-block ui-sortable-handle" style="cursor:grab;"><i class="fa fa-arrows-up-down text-muted"></i></a>
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap ">{./title}</td>
|
||||||
|
<td class="">{./parsedBody}</td>
|
||||||
|
<td class="">
|
||||||
|
<div class="d-flex justify-content-end gap-1">
|
||||||
|
<button data-action="edit" data-key="{./key}" class="btn btn-light btn-sm">[[admin/admin:edit]]</button>
|
||||||
|
<button data-action="delete" data-key="{./key}" class="btn btn-light btn-sm"><i class="fa fa-trash text-danger"></i></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{{ end }}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
11
src/views/admin/partials/manage-ban-reasons-modal.tpl
Normal file
11
src/views/admin/partials/manage-ban-reasons-modal.tpl
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<form>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">[[admin/manage/ban-reasons:reason-title]]</label>
|
||||||
|
<input class="form-control" type="text" name="title" value="{./title}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">[[admin/manage/ban-reasons:reason-body]]</label>
|
||||||
|
<textarea rows="8" class="form-control" type="text" name="body">{./body}</textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -22,9 +22,9 @@
|
|||||||
<p style="margin: 0;">
|
<p style="margin: 0;">
|
||||||
[[email:banned.text3]]
|
[[email:banned.text3]]
|
||||||
</p>
|
</p>
|
||||||
<p style="margin: 0; padding: 6px 0px; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol; font-size: 13px; line-height: 26px; color: #666666;">
|
<div style="margin: 0; padding: 6px 0px; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol; font-size: 13px; line-height: 26px; color: #666666;">
|
||||||
{reason}
|
{reason}
|
||||||
</p>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{{ end }}}
|
{{{ end }}}
|
||||||
|
|||||||
@@ -9,24 +9,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-5">
|
<div class="col-12">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label" for="length">[[admin/manage/users:temp-ban.length]]</label>
|
<label class="form-label" for="length">[[admin/manage/users:temp-ban.length]]</label>
|
||||||
<input class="form-control" id="length" name="length" type="number" min="0" value="0" />
|
<div class="d-flex gap-1">
|
||||||
</div>
|
<input class="form-control" id="length" name="length" type="number" min="0" value="0" />
|
||||||
<div class="form-check form-check-inline">
|
<select class="form-select" id="unit" name="unit">
|
||||||
<label class="form-check-label" for="unit-hours">[[admin/manage/users:temp-ban.hours]]</label>
|
<option value="0">[[admin/manage/users:temp-ban.hours]]</option>
|
||||||
<input class="form-check-input" type="radio" id="unit-hours" name="unit" value="0" checked />
|
<option value="1">[[admin/manage/users:temp-ban.days]]</option>
|
||||||
</div>
|
</select>
|
||||||
<div class="form-check form-check-inline">
|
</div>
|
||||||
<label class="form-check-label" for="unit-days">[[admin/manage/users:temp-ban.days]]</label>
|
|
||||||
<input class="form-check-input" type="radio" id="unit-days" name="unit" value="1" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-7">
|
<div class="col-12">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label" for="reason">[[admin/manage/users:temp-ban.reason]]</label>
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||||
<input type="text" class="form-control" id="reason" name="reason" />
|
<label class="form-label mb-0" for="reason">[[admin/manage/users:temp-ban.reason]]</label>
|
||||||
|
{{{ if reasons.length }}}
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-light btn-sm dropdown-toggle" type="button" id="reasonDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
[[admin/manage/users:temp-ban.select-reason]]
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end p-1" aria-labelledby="reasonDropdown">
|
||||||
|
{{{ each reasons }}}
|
||||||
|
<li><a class="dropdown-item rounded-1" href="#" data-key="{./key}">{./title}</a></li>
|
||||||
|
{{{ end }}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea rows="8" type="text" class="form-control" id="reason" name="reason"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,23 +1,47 @@
|
|||||||
<form class="form">
|
<form class="form">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-5">
|
<div class="col-12">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label" for="length">[[admin/manage/users:temp-ban.length]]</label>
|
<p class="form-text">
|
||||||
<input class="form-control" id="length" name="length" type="number" min="0" value="1" />
|
[[admin/manage/users:temp-mute.explanation]]
|
||||||
</div>
|
</p>
|
||||||
<div class="form-check form-check-inline">
|
|
||||||
<label class="form-check-label" for="unit-hours">[[admin/manage/users:temp-ban.hours]]</label>
|
|
||||||
<input class="form-check-input" type="radio" id="unit-hours" name="unit" value="0" checked />
|
|
||||||
</div>
|
|
||||||
<div class="form-check form-check-inline">
|
|
||||||
<label class="form-check-label" for="unit-days">[[admin/manage/users:temp-ban.days]]</label>
|
|
||||||
<input class="form-check-input" type="radio" id="unit-days" name="unit" value="1" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-7">
|
</div>
|
||||||
<div class="">
|
<div class="row">
|
||||||
<label class="form-label" for="reason">[[admin/manage/users:temp-ban.reason]]</label>
|
<div class="col-12">
|
||||||
<input type="text" class="form-control" id="reason" name="reason" />
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="length">[[admin/manage/users:temp-ban.length]]</label>
|
||||||
|
<div class="d-flex gap-1">
|
||||||
|
<input class="form-control" id="length" name="length" type="number" min="0" value="0" />
|
||||||
|
<select class="form-select" id="unit" name="unit">
|
||||||
|
<option value="0">[[admin/manage/users:temp-ban.hours]]</option>
|
||||||
|
<option value="1">[[admin/manage/users:temp-ban.days]]</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||||
|
<label class="form-label mb-0" for="reason">[[admin/manage/users:temp-ban.reason]]</label>
|
||||||
|
{{{ if reasons.length }}}
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-light btn-sm dropdown-toggle" type="button" id="reasonDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
[[admin/manage/users:temp-ban.select-reason]]
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end p-1" aria-labelledby="reasonDropdown">
|
||||||
|
{{{ each reasons }}}
|
||||||
|
<li><a class="dropdown-item rounded-1" href="#" data-key="{./key}">{./title}</a></li>
|
||||||
|
{{{ end }}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea rows="8" type="text" class="form-control" id="reason" name="reason"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user