mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-03-03 11:01:20 +01:00
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
321 lines
10 KiB
JavaScript
321 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
const validator = require('validator');
|
|
|
|
const user = require('../../user');
|
|
const meta = require('../../meta');
|
|
const db = require('../../database');
|
|
const pagination = require('../../pagination');
|
|
const events = require('../../events');
|
|
const plugins = require('../../plugins');
|
|
const privileges = require('../../privileges');
|
|
const utils = require('../../utils');
|
|
|
|
const usersController = module.exports;
|
|
|
|
const userFields = [
|
|
'uid', 'username', 'userslug', 'email', 'postcount', 'joindate', 'banned',
|
|
'reputation', 'picture', 'flags', 'lastonline', 'email:confirmed',
|
|
];
|
|
|
|
usersController.index = async function (req, res) {
|
|
if (req.query.query) {
|
|
await usersController.search(req, res);
|
|
} else {
|
|
await getUsers(req, res);
|
|
}
|
|
};
|
|
|
|
async function getUsers(req, res) {
|
|
const sortDirection = req.query.sortDirection || 'desc';
|
|
const reverse = sortDirection === 'desc';
|
|
|
|
const page = parseInt(req.query.page, 10) || 1;
|
|
let resultsPerPage = parseInt(req.query.resultsPerPage, 10) || 50;
|
|
if (![50, 100, 250, 500].includes(resultsPerPage)) {
|
|
resultsPerPage = 50;
|
|
}
|
|
let sortBy = validator.escape(req.query.sortBy || '');
|
|
const filterBy = Array.isArray(req.query.filters || []) ? (req.query.filters || []) : [req.query.filters];
|
|
const start = Math.max(0, page - 1) * resultsPerPage;
|
|
const stop = start + resultsPerPage - 1;
|
|
|
|
function buildSet() {
|
|
const sortToSet = {
|
|
postcount: 'users:postcount',
|
|
reputation: 'users:reputation',
|
|
joindate: 'users:joindate',
|
|
lastonline: 'users:online',
|
|
flags: 'users:flags',
|
|
};
|
|
|
|
const set = [];
|
|
if (sortBy) {
|
|
set.push(sortToSet[sortBy]);
|
|
}
|
|
if (filterBy.includes('unverified')) {
|
|
set.push('group:unverified-users:members');
|
|
}
|
|
if (filterBy.includes('verified')) {
|
|
set.push('group:verified-users:members');
|
|
}
|
|
if (filterBy.includes('banned')) {
|
|
set.push('users:banned');
|
|
}
|
|
if (!set.length) {
|
|
set.push('users:online');
|
|
sortBy = 'lastonline';
|
|
}
|
|
return set.length > 1 ? set : set[0];
|
|
}
|
|
|
|
async function getCount(set) {
|
|
if (Array.isArray(set)) {
|
|
return await db.sortedSetIntersectCard(set);
|
|
}
|
|
return await db.sortedSetCard(set);
|
|
}
|
|
|
|
async function getUids(set) {
|
|
let uids = [];
|
|
if (Array.isArray(set)) {
|
|
const weights = set.map((s, index) => (index ? 0 : 1));
|
|
uids = await db[reverse ? 'getSortedSetRevIntersect' : 'getSortedSetIntersect']({
|
|
sets: set,
|
|
start: start,
|
|
stop: stop,
|
|
weights: weights,
|
|
});
|
|
} else {
|
|
uids = await db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop);
|
|
}
|
|
return uids;
|
|
}
|
|
|
|
const set = buildSet();
|
|
const uids = await getUids(set);
|
|
const [count, users, customUserFields] = await Promise.all([
|
|
getCount(set),
|
|
loadUserInfo(req.uid, uids),
|
|
getCustomUserFields(),
|
|
]);
|
|
|
|
await render(req, res, {
|
|
users: users.filter(user => user && parseInt(user.uid, 10)),
|
|
page: page,
|
|
pageCount: Math.max(1, Math.ceil(count / resultsPerPage)),
|
|
resultsPerPage: resultsPerPage,
|
|
reverse: reverse,
|
|
sortBy: sortBy,
|
|
customUserFields,
|
|
});
|
|
}
|
|
|
|
async function getCustomUserFields() {
|
|
const keys = await db.getSortedSetRange('user-custom-fields', 0, -1);
|
|
return (await db.getObjects(keys.map(k => `user-custom-field:${k}`))).filter(Boolean);
|
|
}
|
|
|
|
usersController.search = async function (req, res) {
|
|
const sortDirection = req.query.sortDirection || 'desc';
|
|
const reverse = sortDirection === 'desc';
|
|
const page = parseInt(req.query.page, 10) || 1;
|
|
let resultsPerPage = parseInt(req.query.resultsPerPage, 10) || 50;
|
|
if (![50, 100, 250, 500].includes(resultsPerPage)) {
|
|
resultsPerPage = 50;
|
|
}
|
|
|
|
const searchData = await user.search({
|
|
uid: req.uid,
|
|
query: req.query.query,
|
|
searchBy: req.query.searchBy,
|
|
sortBy: req.query.sortBy,
|
|
sortDirection: sortDirection,
|
|
filters: req.query.filters,
|
|
page: page,
|
|
resultsPerPage: resultsPerPage,
|
|
findUids: async function (query, searchBy, hardCap) {
|
|
if (!query || query.length < 2) {
|
|
return [];
|
|
}
|
|
query = String(query).toLowerCase();
|
|
if (!query.endsWith('*')) {
|
|
query += '*';
|
|
}
|
|
|
|
const data = await db.getSortedSetScan({
|
|
key: `${searchBy}:sorted`,
|
|
match: query,
|
|
limit: hardCap || (resultsPerPage * 10),
|
|
});
|
|
return data.map(data => data.split(':').pop());
|
|
},
|
|
});
|
|
|
|
const uids = searchData.users.map(user => user && user.uid);
|
|
searchData.users = await loadUserInfo(req.uid, uids);
|
|
if (req.query.searchBy === 'ip') {
|
|
searchData.users.forEach((user) => {
|
|
user.ip = user.ips.find(ip => ip.includes(String(req.query.query)));
|
|
});
|
|
}
|
|
searchData.query = validator.escape(String(req.query.query || ''));
|
|
searchData.page = page;
|
|
searchData.resultsPerPage = resultsPerPage;
|
|
searchData.sortBy = req.query.sortBy;
|
|
searchData.reverse = reverse;
|
|
await render(req, res, searchData);
|
|
};
|
|
|
|
async function loadUserInfo(callerUid, uids) {
|
|
async function getIPs() {
|
|
return await Promise.all(uids.map(uid => db.getSortedSetRevRange(`uid:${uid}:ip`, 0, 4)));
|
|
}
|
|
async function getConfirmObjs() {
|
|
const keys = uids.map(uid => `confirm:byUid:${uid}`);
|
|
const codes = await db.mget(keys);
|
|
const confirmObjs = await db.getObjects(codes.map(code => `confirm:${code}`));
|
|
return uids.map((uid, index) => confirmObjs[index]);
|
|
}
|
|
|
|
const [isAdmin, userData, lastonline, confirmObjs, ips] = await Promise.all([
|
|
user.isAdministrator(uids),
|
|
user.getUsersWithFields(uids, userFields, callerUid),
|
|
db.sortedSetScores('users:online', uids),
|
|
getConfirmObjs(),
|
|
getIPs(),
|
|
]);
|
|
userData.forEach((user, index) => {
|
|
if (user) {
|
|
user.administrator = isAdmin[index];
|
|
user.flags = userData[index].flags || 0;
|
|
const timestamp = lastonline[index] || user.joindate;
|
|
user.lastonline = timestamp;
|
|
user.lastonlineISO = utils.toISOString(timestamp);
|
|
user.ips = ips[index];
|
|
user.ip = ips[index] && ips[index][0] ? ips[index][0] : null;
|
|
user.emailToConfirm = user.email;
|
|
if (confirmObjs[index] && confirmObjs[index].email) {
|
|
const confirmObj = confirmObjs[index];
|
|
user['email:expired'] = !confirmObj.expires || Date.now() >= confirmObj.expires;
|
|
user['email:pending'] = confirmObj.expires && Date.now() < confirmObj.expires;
|
|
user.emailToConfirm = validator.escape(String(confirmObj.email));
|
|
}
|
|
}
|
|
});
|
|
return userData;
|
|
}
|
|
|
|
usersController.registrationQueue = async function (req, res) {
|
|
const page = parseInt(req.query.page, 10) || 1;
|
|
const itemsPerPage = 20;
|
|
const start = (page - 1) * 20;
|
|
const stop = start + itemsPerPage - 1;
|
|
|
|
const data = await utils.promiseParallel({
|
|
registrationQueueCount: db.sortedSetCard('registration:queue'),
|
|
users: user.getRegistrationQueue(start, stop),
|
|
customHeaders: plugins.hooks.fire('filter:admin.registrationQueue.customHeaders', { headers: [] }),
|
|
invites: getInvites(),
|
|
});
|
|
const pageCount = Math.max(1, Math.ceil(data.registrationQueueCount / itemsPerPage));
|
|
data.pagination = pagination.create(page, pageCount);
|
|
data.customHeaders = data.customHeaders.headers;
|
|
data.title = '[[pages:registration-queue]]';
|
|
res.render('admin/manage/registration', data);
|
|
};
|
|
|
|
async function getInvites() {
|
|
const invitations = await user.getAllInvites();
|
|
const uids = invitations.map(invite => invite.uid);
|
|
let usernames = await user.getUsersFields(uids, ['username']);
|
|
usernames = usernames.map(user => user.username);
|
|
|
|
invitations.forEach((invites, index) => {
|
|
invites.username = usernames[index];
|
|
});
|
|
|
|
async function getUsernamesByEmails(emails) {
|
|
const uids = await db.sortedSetScores('email:uid', emails.map(email => String(email).toLowerCase()));
|
|
const usernames = await user.getUsersFields(uids, ['username']);
|
|
return usernames.map(user => user.username);
|
|
}
|
|
|
|
usernames = await Promise.all(invitations.map(invites => getUsernamesByEmails(invites.invitations)));
|
|
|
|
invitations.forEach((invites, index) => {
|
|
invites.invitations = invites.invitations.map((email, i) => ({
|
|
email: email,
|
|
username: usernames[index][i] === '[[global:guest]]' ? '' : usernames[index][i],
|
|
}));
|
|
});
|
|
return invitations;
|
|
}
|
|
|
|
async function render(req, res, data) {
|
|
data.pagination = pagination.create(data.page, data.pageCount, req.query);
|
|
|
|
const { registrationType } = meta.config;
|
|
|
|
data.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only';
|
|
data.adminInviteOnly = registrationType === 'admin-invite-only';
|
|
data[`sort_${data.sortBy}`] = true;
|
|
if (req.query.searchBy) {
|
|
data[`searchBy_${validator.escape(String(req.query.searchBy))}`] = true;
|
|
}
|
|
const filterBy = Array.isArray(req.query.filters || []) ? (req.query.filters || []) : [req.query.filters];
|
|
filterBy.forEach((filter) => {
|
|
data[`filterBy_${validator.escape(String(filter))}`] = true;
|
|
});
|
|
data.userCount = parseInt(await db.getObjectField('global', 'userCount'), 10);
|
|
if (data.adminInviteOnly) {
|
|
data.showInviteButton = await privileges.users.isAdministrator(req.uid);
|
|
} else {
|
|
data.showInviteButton = await privileges.users.hasInvitePrivilege(req.uid);
|
|
}
|
|
|
|
res.render('admin/manage/users', data);
|
|
}
|
|
|
|
usersController.getCSV = async function (req, res, next) {
|
|
await events.log({
|
|
type: 'getUsersCSV',
|
|
uid: req.uid,
|
|
ip: req.ip,
|
|
});
|
|
const path = require('path');
|
|
const { baseDir } = require('../../constants').paths;
|
|
res.sendFile('users.csv', {
|
|
root: path.join(baseDir, 'build/export'),
|
|
headers: {
|
|
'Content-Type': 'text/csv',
|
|
'Content-Disposition': 'attachment; filename=users.csv',
|
|
},
|
|
}, (err) => {
|
|
if (err) {
|
|
if (err.code === 'ENOENT') {
|
|
res.locals.isAPI = false;
|
|
return next();
|
|
}
|
|
return next(err);
|
|
}
|
|
});
|
|
};
|
|
|
|
usersController.customFields = async function (req, res) {
|
|
const keys = await db.getSortedSetRange('user-custom-fields', 0, -1);
|
|
const fields = (await db.getObjects(keys.map(k => `user-custom-field:${k}`))).filter(Boolean);
|
|
fields.forEach((field) => {
|
|
if (field['select-options']) {
|
|
field.selectOptionsFormatted = field['select-options'].trim().split('\n').join(', ');
|
|
}
|
|
field['min:rep'] = field['min:rep'] || 0;
|
|
field.visibility = field.visibility || 'all';
|
|
});
|
|
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 });
|
|
}; |