From a1ddc210b295b6527b5f0349c997c0b5d5e4612c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 31 Mar 2020 17:06:13 -0400 Subject: [PATCH] feat: added DELETE /api/v1/users/:uid and DELETE /api/v1/users --- openapi.yaml | 350 +++++++++++++-------- public/src/client/account/edit.js | 18 +- public/src/client/account/edit/email.js | 14 +- public/src/client/account/edit/username.js | 15 +- src/controllers/helpers.js | 10 +- src/controllers/write/users.js | 103 +++++- src/middleware/expose.js | 40 +++ src/middleware/index.js | 1 + src/routes/helpers.js | 7 +- src/routes/write/users.js | 35 ++- src/socket.io/admin/user.js | 2 + src/socket.io/user/profile.js | 5 + 12 files changed, 431 insertions(+), 169 deletions(-) create mode 100644 src/middleware/expose.js diff --git a/openapi.yaml b/openapi.yaml index 58ef118d0063..c87a70fd7871 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: - description: Standard, out-of-the-box read & write API for NodeBB v2.0+ - version: "1.0.0" + description: 'Standard, out-of-the-box read & write API for NodeBB v2.0+' + version: 31-03-2020 title: Read/Write API contact: email: support@nodebb.org @@ -9,80 +9,14 @@ info: name: MIT url: 'https://opensource.org/licenses/MIT' tags: - # - name: admins - # description: Secured Admin-only calls - # - name: developers - # description: Operations available to regular developers - name: users - description: Account related calls (create, modify, delete, etc.) + description: 'Account related calls (create, modify, delete, etc.)' paths: - # /inventory: - # get: - # tags: - # - developers - # summary: searches inventory - # operationId: searchInventory - # description: | - # By passing in the appropriate options, you can search for - # available inventory in the system - # parameters: - # - in: query - # name: searchString - # description: pass an optional search string for looking up inventory - # required: false - # schema: - # type: string - # - in: query - # name: skip - # description: number of records to skip for pagination - # schema: - # type: integer - # format: int32 - # minimum: 0 - # - in: query - # name: limit - # description: maximum number of records to return - # schema: - # type: integer - # format: int32 - # minimum: 0 - # maximum: 50 - # responses: - # '200': - # description: search results matching criteria - # content: - # application/json: - # schema: - # type: array - # items: - # $ref: '#/components/schemas/InventoryItem' - # '400': - # description: bad input parameter - # post: - # tags: - # - admins - # summary: adds an inventory item - # operationId: addInventory - # description: Adds an item to the system - # responses: - # '201': - # description: item created - # '400': - # description: 'invalid input, object invalid' - # '409': - # description: an existing item already exists - # requestBody: - # content: - # application/json: - # schema: - # $ref: '#/components/schemas/InventoryItem' - # description: Inventory item to add /: post: tags: - users summary: creates a user account - operationId: createUser description: This operation creates a new user account requestBody: required: true @@ -93,7 +27,7 @@ paths: properties: username: type: string - description: If the username is taken, a number will be appended + description: 'If the username is taken, a number will be appended' password: type: string email: @@ -110,46 +44,144 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/User' + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + $ref: '#/components/schemas/UserObj' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '426': + $ref: '#/components/responses/426' + '500': + $ref: '#/components/responses/500' + delete: + tags: + - users + summary: deletes one or more users + description: This operation deletes one or many user accounts, including their contributions (posts, topics, etc.) + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + uids: + type: array + description: A collection of uids + example: + uids: + - 1 + - 2 + - 3 + responses: + '200': + description: user account(s) deleted + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + type: object + '/{uid}': + put: + tags: + - users + summary: updates a user account + parameters: + - in: path + name: uid + schema: + type: integer + required: true + description: uid of the user to update + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UserRequest' + responses: + '200': + description: user profile updated + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + $ref: '#/components/schemas/UserObj' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '426': + $ref: '#/components/responses/426' + '500': + $ref: '#/components/responses/500' + delete: + tags: + - users + summary: delete a single user account + parameters: + - in: path + name: uid + schema: + type: integer + required: true + description: uid of the user to delete + responses: + '200': + description: user account deleted + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + type: object components: schemas: - # InventoryItem: - # type: object - # required: - # - id - # - name - # - manufacturer - # - releaseDate - # properties: - # id: - # type: string - # format: uuid - # example: d290f1ee-6c54-4b01-90e6-d701748f0851 - # name: - # type: string - # example: Widget Adapter - # releaseDate: - # type: string - # format: date-time - # example: '2016-08-29T09:12:33.001Z' - # manufacturer: - # $ref: '#/components/schemas/Manufacturer' - # Manufacturer: - # required: - # - name - # properties: - # name: - # type: string - # example: ACME Corporation - # homePage: - # type: string - # format: url - # example: 'https://www.acme-corp.com' - # phone: - # type: string - # example: 408-867-5309 - # type: object - User: + Status: + type: object + properties: + code: + allOf: + - title: Success + type: string + example: ok + - title: Error + type: string + example: error + message: + type: string + example: OK + Error: + type: object + properties: + status: + type: object + properties: + code: + type: string + message: + type: string + response: + type: object + UserObj: properties: uid: type: number @@ -163,7 +195,7 @@ components: email: type: string example: dragonfruit@example.org - email:confirmed: + 'email:confirmed': type: number example: 1 joindate: @@ -174,20 +206,20 @@ components: example: 1585337827953 picture: type: string - example: https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80 + example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80' fullname: type: string example: Mr. Dragon Fruit Jr. location: type: string - example: Toronto, Canada + example: 'Toronto, Canada' birthday: type: string description: A birthdate given in an ISO format parseable by the Date object example: 03/27/2020 website: type: string - example: https://example.org + example: 'https://example.org' aboutme: type: string example: | @@ -202,7 +234,7 @@ components: uploadedpicture: type: string example: /assets/profile/1-profileimg.png - description: In almost all cases, defer to "picture" instead. Use this if you need to specifically reference the picture uploaded to the forum. + description: 'In almost all cases, defer to "picture" instead. Use this if you need to specifically reference the picture uploaded to the forum.' profileviews: type: number example: 1000 @@ -221,7 +253,7 @@ components: banned: type: number example: 0 - banned:expire: + 'banned:expire': type: number example: 1585337827953 status: @@ -236,35 +268,107 @@ components: followingcount: type: number example: 5 - cover:url: + 'cover:url': type: string example: /assets/profile/1-cover.png - cover:position: + 'cover:position': type: string example: 50.0301% 19.2464% groupTitle: type: string - example: "[\"administrators\",\"Staff\"]" + example: '["administrators","Staff"]' groupTitleArray: type: array example: - administrators - Staff - icon:text: + 'icon:text': type: string example: D - icon:bgColor: + 'icon:bgColor': type: string - example: "#9c27b0" + example: '#9c27b0' joindateISO: type: string - example: 2020-03-27T20:30:36.590Z + example: '2020-03-27T20:30:36.590Z' lastonlineISO: type: string - example: 2020-03-27T20:30:36.590Z + example: '2020-03-27T20:30:36.590Z' banned_until: type: number example: 0 banned_until_readable: type: string - example: Not Banned \ No newline at end of file + example: Not Banned + UserRequest: + properties: + username: + type: string + example: Dragon Fruit + email: + type: string + example: dragonfruit@example.org + fullname: + type: string + example: Mr. Dragon Fruit Jr. + website: + type: string + example: 'https://example.org' + location: + type: string + example: 'Toronto, Canada' + groupTitle: + type: string + example: '["administrators","Staff"]' + birthday: + type: string + description: A birthdate given in an ISO format parseable by the Date object + example: 03/27/2020 + signature: + type: string + example: | + This is an example signature + It can span multiple lines. + aboutme: + type: string + example: | + This is a paragraph all about how my life got twist-turned upside-down + and I'd like to take a minute and sit right here, + to tell you all about how I because the administrator of NodeBB + responses: + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Not Authorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '426': + description: Upgrade Required + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' diff --git a/public/src/client/account/edit.js b/public/src/client/account/edit.js index aeb63b756156..b075a18302f8 100644 --- a/public/src/client/account/edit.js +++ b/public/src/client/account/edit.js @@ -42,18 +42,20 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components' $(window).trigger('action:profile.update', userData); - socket.emit('user.updateProfile', userData, function (err, data) { - if (err) { - return app.alertError(err.message); - } - + $.ajax({ + url: config.relative_path + '/api/v1/users/' + userData.uid, + data: userData, + method: 'put', + }).done(function (res) { app.alertSuccess('[[user:profile_update_success]]'); - if (data.picture) { - $('#user-current-picture').attr('src', data.picture); + if (res.response.picture) { + $('#user-current-picture').attr('src', res.response.picture); } - updateHeader(data.picture); + updateHeader(res.response.picture); + }).fail(function (ev) { + return app.alertError(ev.responseJSON.status.message); }); return false; diff --git a/public/src/client/account/edit/email.js b/public/src/client/account/edit/email.js index 28bac3797837..aa905f082463 100644 --- a/public/src/client/account/edit/email.js +++ b/public/src/client/account/edit/email.js @@ -27,13 +27,15 @@ define('forum/account/edit/email', ['forum/account/header'], function (header) { var btn = $(this); btn.addClass('disabled').find('i').removeClass('hide'); - socket.emit('user.changeUsernameEmail', userData, function (err) { + $.ajax({ + url: config.relative_path + '/api/v1/users/' + userData.uid, + data: userData, + method: 'put', + }).done(function (res) { btn.removeClass('disabled').find('i').addClass('hide'); - if (err) { - return app.alertError(err.message); - } - - ajaxify.go('user/' + ajaxify.data.userslug + '/edit'); + ajaxify.go('user/' + res.response.userslug + '/edit'); + }).fail(function (ev) { + app.alertError(ev.responseJSON.status.message); }); return false; diff --git a/public/src/client/account/edit/username.js b/public/src/client/account/edit/username.js index 6178466f24b5..4cb60cf217a2 100644 --- a/public/src/client/account/edit/username.js +++ b/public/src/client/account/edit/username.js @@ -24,22 +24,25 @@ define('forum/account/edit/username', ['forum/account/header'], function (header var btn = $(this); btn.addClass('disabled').find('i').removeClass('hide'); - socket.emit('user.changeUsernameEmail', userData, function (err, data) { - btn.removeClass('disabled').find('i').addClass('hide'); - if (err) { - return app.alertError(err.message); - } + $.ajax({ + url: config.relative_path + '/api/v1/users/' + userData.uid, + data: userData, + method: 'put', + }).done(function (res) { + btn.removeClass('disabled').find('i').addClass('hide'); var userslug = utils.slugify(userData.username); if (userData.username && userslug && parseInt(userData.uid, 10) === parseInt(app.user.uid, 10)) { $('[component="header/profilelink"]').attr('href', config.relative_path + '/user/' + userslug); $('[component="header/profilelink/edit"]').attr('href', config.relative_path + '/user/' + userslug + '/edit'); $('[component="header/profilelink/settings"]').attr('href', config.relative_path + '/user/' + userslug + '/settings'); $('[component="header/username"]').text(userData.username); - $('[component="header/usericon"]').css('background-color', data['icon:bgColor']).text(data['icon:text']); + $('[component="header/usericon"]').css('background-color', res.response['icon:bgColor']).text(res.response['icon:text']); } ajaxify.go('user/' + userslug + '/edit'); + }).fail(function (ev) { + app.alertError(ev.responseJSON.status.message); }); return false; diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index fa3ef39b1198..9891cb5912b1 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -349,12 +349,16 @@ helpers.formatApiResponse = async (statusCode, res, payload) => { response: payload || {}, }); } else if (payload instanceof Error) { + let message = ''; if (isLanguageKey.test(payload.message)) { - const translated = await translator.translate(payload.message, 'en-GB'); - res.status(statusCode).json(helpers.generateError(statusCode, translated)); + message = await translator.translate(payload.message, 'en-GB'); } else { - res.status(statusCode).json(helpers.generateError(statusCode, payload.message)); + message = payload.message; } + + const returnPayload = helpers.generateError(statusCode, message); + returnPayload.stack = payload.stack; + res.status(statusCode).json(returnPayload); } else if (!payload) { // Non-2xx statusCode, generate predefined error res.status(statusCode).json(helpers.generateError(statusCode)); diff --git a/src/controllers/write/users.js b/src/controllers/write/users.js index f98fd735739c..0b996c439683 100644 --- a/src/controllers/write/users.js +++ b/src/controllers/write/users.js @@ -1,11 +1,108 @@ 'use strict'; -const users = require('../../user'); +const user = require('../../user'); +const groups = require('../../groups'); +const privileges = require('../../privileges'); +const meta = require('../../meta'); +const events = require('../../events'); const helpers = require('../helpers'); const Users = module.exports; Users.create = async (req, res) => { - const uid = await users.create(req.body); - helpers.formatApiResponse(200, res, await users.getUserData(uid)); + const uid = await user.create(req.body); + helpers.formatApiResponse(200, res, await user.getUserData(uid)); }; + +Users.update = async (req, res) => { + const oldUserData = await user.getUserFields(req.params.uid, ['email', 'username']); + if (!oldUserData || !oldUserData.username) { + throw new Error('[[error:invalid-data]]'); + } + + const [isAdminOrGlobalMod, canEdit, passwordMatch] = await Promise.all([ + user.isAdminOrGlobalMod(req.user.uid), + privileges.users.canEdit(req.user.uid, req.params.uid), + user.isPasswordCorrect(req.body.uid, req.body.password, req.ip), + ]); + + // Changing own email/username requires password confirmation + if (req.user.uid === req.body.uid && !passwordMatch) { + helpers.formatApiResponse(403, res, new Error('[[error:invalid-password]]')); + } + + if (!canEdit) { + helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]')); + } + + if (!isAdminOrGlobalMod && meta.config['username:disableEdit']) { + req.body.username = oldUserData.username; + } + + if (!isAdminOrGlobalMod && meta.config['email:disableEdit']) { + req.body.email = oldUserData.email; + } + + req.body.uid = req.params.uid; // The `uid` argument in `updateProfile` refers to calling user, not target user + await user.updateProfile(req.user.uid, req.body); + const userData = await user.getUserData(req.body.uid); + + async function log(type, eventData) { + eventData.type = type; + eventData.uid = req.user.uid; + eventData.targetUid = req.params.uid; + eventData.ip = req.ip; + await events.log(eventData); + } + + if (userData.email !== oldUserData.email) { + await log('email-change', { oldEmail: oldUserData.email, newEmail: userData.email }); + } + + if (userData.username !== oldUserData.username) { + await log('username-change', { oldUsername: oldUserData.username, newUsername: userData.username }); + } + + helpers.formatApiResponse(200, res, userData); +}; + +Users.delete = async (req, res) => { + processDeletion(req.params.uid, req, res); + helpers.formatApiResponse(200, res); +}; + +Users.deleteMany = async (req, res) => { + await canDeleteUids(req.body.uids, res); + await Promise.all(req.body.uids.map(uid => processDeletion(uid, req, res))); + helpers.formatApiResponse(200, res); +}; + +async function canDeleteUids(uids, res) { + if (!Array.isArray(uids)) { + helpers.formatApiResponse(400, res, new Error('[[error:invalid-data]]')); + } + const isMembers = await groups.isMembers(uids, 'administrators'); + if (isMembers.includes(true)) { + helpers.formatApiResponse(403, res, new Error('[[error:cant-delete-other-admins]]')); + } +} + +async function processDeletion(uid, req, res) { + const isTargetAdmin = await user.isAdministrator(uid); + if (!res.locals.privileges.isSelf && !res.locals.privileges.isAdmin) { + return helpers.formatApiResponse(403, res); + } else if (!res.locals.privileges.isSelf && isTargetAdmin) { + return helpers.formatApiResponse(403, res, new Error('[[error:cant-delete-other-admins]]')); + } + + // TODO: clear user tokens for this uid + const userData = await user.delete(req.user.uid, uid); + await events.log({ + type: 'user-delete', + uid: req.user.uid, + targetUid: uid, + ip: req.ip, + username: userData.username, + email: userData.email, + }); +} diff --git a/src/middleware/expose.js b/src/middleware/expose.js new file mode 100644 index 000000000000..be4a7227127f --- /dev/null +++ b/src/middleware/expose.js @@ -0,0 +1,40 @@ +'use strict'; + +/** + * The middlewares here strictly act to "expose" certain values from the database, + * into `res.locals` for use in middlewares and/or controllers down the line + */ + +const user = require('../user'); +const utils = require('../utils'); + +module.exports = function (middleware) { + middleware.exposeAdmin = async (req, res, next) => { + // Unlike `requireAdmin`, this middleware just checks the uid, and sets `isAdmin` in `res.locals` + res.locals.isAdmin = false; + + if (!req.user) { + return next(); + } + + const isAdmin = await user.isAdministrator(req.user.uid); + res.locals.isAdmin = isAdmin; + return next(); + }; + + middleware.exposePrivileges = async (req, res, next) => { + // Exposes a hash of user's ranks (admin, gmod, etc.) + const hash = await utils.promiseParallel({ + isAdmin: user.isAdministrator(req.user.uid), + isGmod: user.isGlobalModerator(req.user.uid), + isPrivileged: user.isPrivileged(req.user.uid), + }); + + if (req.params.uid) { + hash.isSelf = parseInt(req.params.uid, 10) === req.user.uid; + } + + res.locals.privileges = hash; + return next(); + }; +}; diff --git a/src/middleware/index.js b/src/middleware/index.js index 60f9ebf4faa6..b3d3ea4915f0 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -59,6 +59,7 @@ require('./render')(middleware); require('./maintenance')(middleware); require('./user')(middleware); require('./headers')(middleware); +require('./expose')(middleware); middleware.stripLeadingSlashes = function stripLeadingSlashes(req, res, next) { var target = req.originalUrl.replace(nconf.get('relative_path'), ''); diff --git a/src/routes/helpers.js b/src/routes/helpers.js index 7310037c2ca1..c2c5c3093541 100644 --- a/src/routes/helpers.js +++ b/src/routes/helpers.js @@ -1,6 +1,7 @@ 'use strict'; -var helpers = module.exports; +const helpers = module.exports; +const controllerHelpers = require('../controllers/helpers'); helpers.setupPageRoute = function (router, name, middleware, middlewares, controller) { middlewares = [middleware.maintenanceMode, middleware.registrationComplete, middleware.pageView, middleware.pluginHooks].concat(middlewares); @@ -15,8 +16,8 @@ helpers.setupAdminPageRoute = function (router, name, middleware, middlewares, c }; helpers.setupApiRoute = function (router, name, middleware, middlewares, verb, controller) { - router[verb](name, middleware.authenticate, middlewares, helpers.tryRoute(controller, (err, res) => { - helpers.formatApiResponse(400, res, err); + router[verb](name, middlewares, helpers.tryRoute(controller, (err, res) => { + controllerHelpers.formatApiResponse(400, res, err); })); }; diff --git a/src/routes/write/users.js b/src/routes/write/users.js index dddcf977e886..045dae2abb56 100644 --- a/src/routes/write/users.js +++ b/src/routes/write/users.js @@ -1,8 +1,11 @@ 'use strict'; +const router = require('express').Router(); const middleware = require('../../middleware'); const controllers = require('../../controllers'); -const routeHelpers = require('../../routes/helpers'); +const routeHelpers = require('../helpers'); + +const setupApiRoute = routeHelpers.setupApiRoute; // Messaging = require.main.require('./src/messaging'), // apiMiddleware = require('./middleware'), // errorHandler = require('../../lib/errorHandler'), @@ -10,26 +13,20 @@ const routeHelpers = require('../../routes/helpers'); // utils = require('./utils'), // async = require.main.require('async'); +// eslint-disable-next-line no-unused-vars +function guestRoutes() { + // like registration, login... +} -module.exports = function () { - const router = require('express').Router(); - const setupApiRoute = routeHelpers.setupApiRoute; +function authenticatedRoutes() { + const middlewares = [middleware.authenticate]; - setupApiRoute(router, '/', middleware, [middleware.checkRequired.bind(null, ['username']), middleware.isAdmin], 'post', controllers.write.users.create); + setupApiRoute(router, '/', middleware, [...middlewares, middleware.checkRequired.bind(null, ['username']), middleware.isAdmin], 'post', controllers.write.users.create); + setupApiRoute(router, '/', middleware, [...middlewares, middleware.checkRequired.bind(null, ['uids']), middleware.isAdmin, middleware.exposePrivileges], 'delete', controllers.write.users.deleteMany); + setupApiRoute(router, '/:uid', middleware, [...middlewares], 'put', controllers.write.users.update); + setupApiRoute(router, '/:uid', middleware, [...middlewares, middleware.exposePrivileges], 'delete', controllers.write.users.delete); // app.route('/:uid') - // .put(apiMiddleware.requireUser, apiMiddleware.exposeAdmin, function(req, res) { - // if (parseInt(req.params.uid, 10) !== parseInt(req.user.uid, 10) && !res.locals.isAdmin) { - // return errorHandler.respond(401, res); - // } - - // // `uid` in `updateProfile` refers to calling user, not target user - // req.body.uid = req.params.uid; - - // Users.updateProfile(req.user.uid, req.body, function(err) { - // return errorHandler.handle(err, res); - // }); - // }) // .delete(apiMiddleware.requireUser, apiMiddleware.exposeAdmin, function(req, res) { // if (parseInt(req.params.uid, 10) !== parseInt(req.user.uid, 10) && !res.locals.isAdmin) { // return errorHandler.respond(401, res); @@ -162,6 +159,10 @@ module.exports = function () { // errorHandler.handle(err, res); // }); // }); +} + +module.exports = function () { + authenticatedRoutes(); return router; }; diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js index b458d4fcbe98..bc18d26c438b 100644 --- a/src/socket.io/admin/user.js +++ b/src/socket.io/admin/user.js @@ -139,6 +139,8 @@ User.deleteUsersContent = async function (socket, uids) { }; User.deleteUsersAndContent = async function (socket, uids) { + sockets.warnDeprecated(socket, 'DELETE /api/v1/users or DELETE /api/v1/users/:uid'); + await canDeleteUids(uids); deleteUsers(socket, uids, async function (uid) { return await user.delete(socket.uid, uid); diff --git a/src/socket.io/user/profile.js b/src/socket.io/user/profile.js index 1c2996b3893e..45e2c3bcee02 100644 --- a/src/socket.io/user/profile.js +++ b/src/socket.io/user/profile.js @@ -9,9 +9,12 @@ const privileges = require('../../privileges'); const notifications = require('../../notifications'); const db = require('../../database'); const plugins = require('../../plugins'); +const sockets = require('..'); module.exports = function (SocketUser) { SocketUser.changeUsernameEmail = async function (socket, data) { + sockets.warnDeprecated(socket, 'PUT /api/v1/users/:uid'); + if (!data || !data.uid || !socket.uid) { throw new Error('[[error:invalid-data]]'); } @@ -92,6 +95,8 @@ module.exports = function (SocketUser) { }; SocketUser.updateProfile = async function (socket, data) { + sockets.warnDeprecated(socket, 'PUT /api/v1/users/:uid'); + if (!socket.uid) { throw new Error('[[error:invalid-uid]]'); }