From a5af2dc819ddbea1e35cc3ff6476b16d213d71ae Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 1 Apr 2020 21:15:32 -0400 Subject: [PATCH] feat: added PUT/DELETE /api/v1/users/:uid/ban routes --- openapi.yaml | 69 +++++++++++++++++-- public/src/admin/manage/users.js | 61 ++++++++++++++++- public/src/client/account/header.js | 29 ++++---- src/controllers/write/users.js | 63 +++++++++++++++++ src/routes/write/users.js | 101 +++++++++++++--------------- src/socket.io/user/ban.js | 23 ++----- src/user/bans.js | 21 ++++++ 7 files changed, 273 insertions(+), 94 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index e58eca56cc..ebdffdb79b 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -242,19 +242,74 @@ paths: $ref: '#/components/schemas/Status' response: type: object + '/{uid}/ban': + put: + tags: + - users + summary: bans a user + parameters: + - in: path + name: uid + schema: + type: integer + required: true + description: uid of the user to ban + requestBody: + content: + application/json: + schema: + type: object + properties: + until: + type: number + description: UNIX timestamp of the ban expiry + example: 1585775608076 + reason: + type: string + example: the reason for the ban + responses: + '200': + description: successfully banned user + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + type: object + delete: + tags: + - users + summary: unbans a user + parameters: + - in: path + name: uid + schema: + type: integer + required: true + description: uid of the user to unban + responses: + '200': + description: successfully unbanned user + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + type: object components: schemas: Status: type: object properties: code: - allOf: - - title: Success - type: string - example: ok - - title: Error - type: string - example: error + type: string + example: ok message: type: string example: OK diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index f3a6a77626..3a3dd11fb8 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -44,6 +44,7 @@ define('admin/manage/users', ['translator', 'benchpress', 'autocomplete'], funct $('.users-table [component="user/select/single"]:checked').parents('.user-row').remove(); } + // use onSuccess/onFail instead function done(successMessage, className, flag) { return function (err) { if (err) { @@ -57,6 +58,18 @@ define('admin/manage/users', ['translator', 'benchpress', 'autocomplete'], funct }; } + function onSuccess(successMessage, className, flag) { + app.alertSuccess(successMessage); + if (className) { + update(className, flag); + } + unselectAll(); + } + + function onFail(err) { + app.alertError(err.message); + } + $('[component="user/select/all"]').on('click', function () { $('.users-table [component="user/select/single"]').prop('checked', $(this).is(':checked')); }); @@ -119,7 +132,20 @@ define('admin/manage/users', ['translator', 'benchpress', 'autocomplete'], funct bootbox.confirm((uids.length > 1 ? '[[admin/manage/users:alerts.confirm-ban-multi]]' : '[[admin/manage/users:alerts.confirm-ban]]'), function (confirm) { if (confirm) { - socket.emit('user.banUsers', { uids: uids, reason: '' }, done('[[admin/manage/users:alerts.ban-success]]', '.ban', true)); + var requests = uids.map(function (uid) { + return $.ajax({ + url: config.relative_path + '/api/v1/users/' + uid + '/ban', + method: 'put', + }); + }); + + $.when(requests) + .done(function () { + onSuccess('[[admin/manage/users:alerts.ban-success]]', '.ban', true); + }) + .fail(function (ev) { + onFail(ev.responseJSON.status); + }); } }); }); @@ -150,7 +176,24 @@ define('admin/manage/users', ['translator', 'benchpress', 'autocomplete'], funct return data; }, {}); var until = formData.length > 0 ? (Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1))) : 0; - socket.emit('user.banUsers', { uids: uids, until: until, reason: formData.reason }, done('[[admin/manage/users:alerts.ban-success]]', '.ban', true)); + + var requests = uids.map(function (uid) { + return $.ajax({ + url: config.relative_path + '/api/v1/users/' + uid + '/ban', + method: 'put', + data: { + until: until, + reason: formData.reason, + }, + }); + }); + + $.when(requests) + .done(function () { + onSuccess('[[admin/manage/users:alerts.ban-success]]', '.ban', true); + }).fail(function (ev) { + onFail(ev.responseJSON.status); + }); }, }, }, @@ -165,7 +208,19 @@ define('admin/manage/users', ['translator', 'benchpress', 'autocomplete'], funct return false; // specifically to keep the menu open } - socket.emit('user.unbanUsers', uids, done('[[admin/manage/users:alerts.unban-success]]', '.ban', false)); + var requests = uids.map(function (uid) { + return $.ajax({ + url: config.relative_path + '/api/v1/users/' + uid + '/ban', + method: 'delete', + }); + }); + + $.when(requests) + .done(function () { + onSuccess('[[admin/manage/users:alerts.unban-success]]', '.ban', false); + }).fail(function (ev) { + onFail(ev.responseJSON.status); + }); }); $('.reset-lockout').on('click', function () { diff --git a/public/src/client/account/header.js b/public/src/client/account/header.js index ba48c95713..95315d2918 100644 --- a/public/src/client/account/header.js +++ b/public/src/client/account/header.js @@ -154,20 +154,21 @@ define('forum/account/header', [ var until = formData.length > 0 ? (Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1))) : 0; - socket.emit('user.banUsers', { - uids: [theirid], - until: until, - reason: formData.reason || '', - }, function (err) { - if (err) { - return app.alertError(err.message); - } - + $.ajax({ + url: config.relative_path + '/api/v1/users/' + theirid + '/ban', + method: 'put', + data: { + until: until, + reason: formData.reason || '', + }, + }).done(function () { if (typeof onSuccess === 'function') { return onSuccess(); } ajaxify.refresh(); + }).fail(function (ev) { + app.alertError(ev.responseJSON.status.message); }); }, }, @@ -177,11 +178,13 @@ define('forum/account/header', [ } function unbanAccount() { - socket.emit('user.unbanUsers', [ajaxify.data.theirid], function (err) { - if (err) { - return app.alertError(err.message); - } + $.ajax({ + url: config.relative_path + '/api/v1/users/' + ajaxify.data.theirid + '/ban', + method: 'delete', + }).done(function () { ajaxify.refresh(); + }).fail(function (ev) { + app.alertError(ev.responseJSON.status.message); }); } diff --git a/src/controllers/write/users.js b/src/controllers/write/users.js index edd12b32f6..74f358c2f7 100644 --- a/src/controllers/write/users.js +++ b/src/controllers/write/users.js @@ -7,7 +7,11 @@ const privileges = require('../../privileges'); const notifications = require('../../notifications'); const meta = require('../../meta'); const events = require('../../events'); +const translator = require('../../translator'); + +const db = require('../../database'); const helpers = require('../helpers'); +const sockets = require('../../socket.io'); const Users = module.exports; @@ -155,3 +159,62 @@ Users.unfollow = async (req, res) => { }); helpers.formatApiResponse(200, res); }; + +Users.ban = async (req, res) => { + if (!await privileges.users.hasBanPrivilege(req.user.uid)) { + return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]')); + } else if (await user.isAdministrator(req.params.uid)) { + return helpers.formatApiResponse(403, res, new Error('[[error:cant-ban-other-admins]]')); + } + + const banData = await user.bans.ban(req.params.uid, req.body.until, req.body.reason); + await db.setObjectField('uid:' + req.params.uid + ':ban:' + banData.timestamp, 'fromUid', req.user.uid); + + if (!req.body.reason) { + req.body.reason = await translator.translate('[[user:info.banned-no-reason]]'); + } + + sockets.in('uid_' + req.params.uid).emit('event:banned', { + until: req.body.until, + reason: req.body.reason, + }); + + await events.log({ + type: 'user-ban', + uid: req.user.uid, + targetUid: req.params.uid, + ip: req.ip, + reason: req.body.reason || undefined, + }); + plugins.fireHook('action:user.banned', { + callerUid: req.user.uid, + ip: req.ip, + uid: req.params.uid, + until: req.body.until > 0 ? req.body.until : undefined, + reason: req.body.reason || undefined, + }); + await user.auth.revokeAllSessions(req.params.uid); + + helpers.formatApiResponse(200, res); +}; + +Users.unban = async (req, res) => { + if (!await privileges.users.hasBanPrivilege(req.user.uid)) { + return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]')); + } + + await user.bans.unban(req.params.uid); + await events.log({ + type: 'user-unban', + uid: req.user.uid, + targetUid: req.params.uid, + ip: req.ip, + }); + plugins.fireHook('action:user.unbanned', { + callerUid: req.user.uid, + ip: req.ip, + uid: req.params.uid, + }); + + helpers.formatApiResponse(200, res); +}; diff --git a/src/routes/write/users.js b/src/routes/write/users.js index bbf3079fbb..5835d010ce 100644 --- a/src/routes/write/users.js +++ b/src/routes/write/users.js @@ -32,59 +32,8 @@ function authenticatedRoutes() { setupApiRoute(router, '/:uid/follow', middleware, [...middlewares], 'post', controllers.write.users.follow); setupApiRoute(router, '/:uid/follow', middleware, [...middlewares], 'delete', controllers.write.users.unfollow); - // app.put('/:uid/follow', apiMiddleware.requireUser, function(req, res) { - // Users.follow(req.user.uid, req.params.uid, function(err) { - // return errorHandler.handle(err, res); - // }); - // }); - - // app.delete('/:uid/follow', apiMiddleware.requireUser, function(req, res) { - // Users.unfollow(req.user.uid, req.params.uid, function(err) { - // return errorHandler.handle(err, res); - // }); - // }); - - // app.route('/:uid/chats') - // .post(apiMiddleware.requireUser, function(req, res) { - // if (!utils.checkRequired(['message'], req, res)) { - // return false; - // } - - // var timestamp = parseInt(req.body.timestamp, 10) || Date.now(); - - // function addMessage(roomId) { - // Messaging.addMessage({ - // uid: req.user.uid, - // roomId: roomId, - // content: req.body.message, - // timestamp: timestamp, - // }, function(err, message) { - // if (parseInt(req.body.quiet, 10) !== 1) { - // Messaging.notifyUsersInRoom(req.user.uid, roomId, message); - // } - - // return errorHandler.handle(err, res, message); - // }); - // } - - // Messaging.canMessageUser(req.user.uid, req.params.uid, function(err) { - // if (err) { - // return errorHandler.handle(err, res); - // } - - // if (req.body.roomId) { - // addMessage(req.body.roomId); - // } else { - // Messaging.newRoom(req.user.uid, [req.params.uid], function(err, roomId) { - // if (err) { - // return errorHandler.handle(err, res); - // } - - // addMessage(roomId); - // }); - // } - // }); - // }); + setupApiRoute(router, '/:uid/ban', middleware, [...middlewares, middleware.exposePrivileges], 'put', controllers.write.users.ban); + setupApiRoute(router, '/:uid/ban', middleware, [...middlewares, middleware.exposePrivileges], 'delete', controllers.write.users.unban); // app.route('/:uid/ban') // .put(apiMiddleware.requireUser, apiMiddleware.requireAdmin, function(req, res) { @@ -131,6 +80,52 @@ function authenticatedRoutes() { // errorHandler.handle(err, res); // }); // }); + + /** + * Chat routes were not migrated because chats may get refactored... also the logic is derpy + * It also does not take into account multiple chats for a given user. + */ + // app.route('/:uid/chats') + // .post(apiMiddleware.requireUser, function(req, res) { + // if (!utils.checkRequired(['message'], req, res)) { + // return false; + // } + + // var timestamp = parseInt(req.body.timestamp, 10) || Date.now(); + + // function addMessage(roomId) { + // Messaging.addMessage({ + // uid: req.user.uid, + // roomId: roomId, + // content: req.body.message, + // timestamp: timestamp, + // }, function(err, message) { + // if (parseInt(req.body.quiet, 10) !== 1) { + // Messaging.notifyUsersInRoom(req.user.uid, roomId, message); + // } + + // return errorHandler.handle(err, res, message); + // }); + // } + + // Messaging.canMessageUser(req.user.uid, req.params.uid, function(err) { + // if (err) { + // return errorHandler.handle(err, res); + // } + + // if (req.body.roomId) { + // addMessage(req.body.roomId); + // } else { + // Messaging.newRoom(req.user.uid, [req.params.uid], function(err, roomId) { + // if (err) { + // return errorHandler.handle(err, res); + // } + + // addMessage(roomId); + // }); + // } + // }); + // }); } module.exports = function () { diff --git a/src/socket.io/user/ban.js b/src/socket.io/user/ban.js index 38331664a4..10f76eca5f 100644 --- a/src/socket.io/user/ban.js +++ b/src/socket.io/user/ban.js @@ -1,21 +1,18 @@ 'use strict'; -const winston = require('winston'); - const db = require('../../database'); const user = require('../../user'); -const meta = require('../../meta'); const websockets = require('../index'); const events = require('../../events'); const privileges = require('../../privileges'); const plugins = require('../../plugins'); -const emailer = require('../../emailer'); const translator = require('../../translator'); -const utils = require('../../../public/src/utils'); const flags = require('../../flags'); module.exports = function (SocketUser) { SocketUser.banUsers = async function (socket, data) { + websockets.warnDeprecated(socket, 'PUT /api/v1/users/:uid/ban'); + if (!data || !Array.isArray(data.uids)) { throw new Error('[[error:invalid-data]]'); } @@ -43,6 +40,8 @@ module.exports = function (SocketUser) { }; SocketUser.unbanUsers = async function (socket, uids) { + websockets.warnDeprecated(socket, 'DELETE /api/v1/users/:uid/ban'); + await toggleBan(socket.uid, uids, async function (uid) { await user.bans.unban(uid); await events.log({ @@ -76,19 +75,7 @@ module.exports = function (SocketUser) { if (isAdmin) { throw new Error('[[error:cant-ban-other-admins]]'); } - const username = await user.getUserField(uid, 'username'); - const siteTitle = meta.config.title || 'NodeBB'; - const data = { - subject: '[[email:banned.subject, ' + siteTitle + ']]', - username: username, - until: until ? utils.toISOString(until) : false, - reason: reason, - }; - try { - await emailer.send('banned', uid, data); - } catch (err) { - winston.error('[emailer.send] ' + err.message); - } + const banData = await user.bans.ban(uid, until, reason); await db.setObjectField('uid:' + uid + ':ban:' + banData.timestamp, 'fromUid', callerUid); diff --git a/src/user/bans.js b/src/user/bans.js index bfa81ed287..464b8300bc 100644 --- a/src/user/bans.js +++ b/src/user/bans.js @@ -1,5 +1,10 @@ 'use strict'; +const winston = require('winston'); + +const meta = require('../meta'); +const utils = require('../utils'); +const emailer = require('../emailer'); const db = require('../database'); module.exports = function (User) { @@ -38,6 +43,22 @@ module.exports = function (User) { } else { await db.sortedSetRemove('users:banned:expire', uid); } + + // Email notification of ban + const username = await User.getUserField(uid, 'username'); + const siteTitle = meta.config.title || 'NodeBB'; + const data = { + subject: '[[email:banned.subject, ' + siteTitle + ']]', + username: username, + until: until ? utils.toISOString(until) : false, + reason: reason, + }; + try { + await emailer.send('banned', uid, data); + } catch (err) { + winston.error('[emailer.send] ' + err.message); + } + return banData; };