From 00b9ca111e3ad5dcc4a4bce76cc062138f84b394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20U=C5=9Fakl=C4=B1?= Date: Sun, 11 Jan 2026 14:38:14 -0500 Subject: [PATCH] Change owner rest route (#13881) * fix: dont use sass-embedded on freebsd, #13867 * fix: #13715, dont reduce hardcap if usersPerPage is < 50 * fix: closes #13872, use translator.compile for notification text so commas don't cause issues * fix: remove bidiControls from notification.bodyShort * refactor: move change owner call to rest api deprecate socket method * fix spec * test: one more fix * test: add 404 * test: fix tests :rage1: * test: update test to use new method --- public/openapi/write.yaml | 4 +++ public/openapi/write/posts/owner.yaml | 39 +++++++++++++++++++++++ public/openapi/write/posts/pid/owner.yaml | 39 +++++++++++++++++++++++ public/src/client/topic/change-owner.js | 11 +++---- src/api/posts.js | 23 +++++++++++++ src/controllers/write/posts.js | 8 +++++ src/routes/write/posts.js | 2 ++ src/socket.io/posts/tools.js | 22 +++---------- test/posts.js | 2 +- 9 files changed, 125 insertions(+), 25 deletions(-) create mode 100644 public/openapi/write/posts/owner.yaml create mode 100644 public/openapi/write/posts/pid/owner.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 7771469725..ef17b75737 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -206,6 +206,10 @@ paths: $ref: 'write/posts/queue/id.yaml' /posts/queue/{id}/notify: $ref: 'write/posts/queue/notify.yaml' + /posts/{pid}/owner: + $ref: 'write/posts/pid/owner.yaml' + /posts/owner: + $ref: 'write/posts/owner.yaml' /chats/: $ref: 'write/chats.yaml' /chats/unread: diff --git a/public/openapi/write/posts/owner.yaml b/public/openapi/write/posts/owner.yaml new file mode 100644 index 0000000000..b69e0b5b44 --- /dev/null +++ b/public/openapi/write/posts/owner.yaml @@ -0,0 +1,39 @@ +post: + tags: + - Posts + summary: Change owner of one or more posts + description: Change the owner of the posts identified by pids to the user uid. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - pids + - uid + properties: + pids: + type: array + items: + type: integer + description: Array of post IDs to change owner for + example: [2] + uid: + type: integer + description: Target user id + example: 1 + responses: + '200': + description: Owner changed successfully + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + type: object + '404': + description: Post not found diff --git a/public/openapi/write/posts/pid/owner.yaml b/public/openapi/write/posts/pid/owner.yaml new file mode 100644 index 0000000000..e49a627963 --- /dev/null +++ b/public/openapi/write/posts/pid/owner.yaml @@ -0,0 +1,39 @@ +put: + summary: Change owner of a post + description: Change the owner (uid) of a post identified by pid. + tags: + - Posts + parameters: + - name: pid + in: path + description: Post id + required: true + schema: + type: integer + example: 2 + requestBody: + description: New owner payload + required: true + content: + application/json: + schema: + type: object + required: + - uid + properties: + uid: + type: integer + description: User id of the new owner + example: 2 + responses: + '200': + description: Owner changed successfully + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object diff --git a/public/src/client/topic/change-owner.js b/public/src/client/topic/change-owner.js index b0b4e5be6c..9be031b62a 100644 --- a/public/src/client/topic/change-owner.js +++ b/public/src/client/topic/change-owner.js @@ -5,7 +5,8 @@ define('forum/topic/change-owner', [ 'postSelect', 'autocomplete', 'alerts', -], function (postSelect, autocomplete, alerts) { + 'api', +], function (postSelect, autocomplete, alerts, api) { const ChangeOwner = {}; let modal; @@ -69,14 +70,12 @@ define('forum/topic/change-owner', [ if (!toUid) { return; } - socket.emit('posts.changeOwner', { pids: postSelect.pids, toUid: toUid }, function (err) { - if (err) { - return alerts.error(err); - } + + api.post('/posts/owner', { pids: postSelect.pids, uid: toUid}).then(() => { ajaxify.go(`/post/${postSelect.pids[0]}`); closeModal(); - }); + }).catch(alerts.error); } function closeModal() { diff --git a/src/api/posts.js b/src/api/posts.js index a7eecaa633..c9a570dec4 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -665,3 +665,26 @@ async function sendQueueNotification(type, targetUid, path, notificationText) { const notifObj = await notifications.create(notifData); await notifications.push(notifObj, [targetUid]); } + +postsAPI.changeOwner = async function (caller, data) { + if (!data || !Array.isArray(data.pids) || !data.uid) { + throw new Error('[[error:invalid-data]]'); + } + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(caller.uid); + if (!isAdminOrGlobalMod) { + throw new Error('[[error:no-privileges]]'); + } + + const postData = await posts.changeOwner(data.pids, data.uid); + const logs = postData.map(({ pid, uid, cid }) => (events.log({ + type: 'post-change-owner', + uid: caller.uid, + ip: caller.ip, + targetUid: data.uid, + pid: pid, + originalUid: uid, + cid: cid, + }))); + + await Promise.all(logs); +}; \ No newline at end of file diff --git a/src/controllers/write/posts.js b/src/controllers/write/posts.js index 5828b44704..9e8053d17d 100644 --- a/src/controllers/write/posts.js +++ b/src/controllers/write/posts.js @@ -209,4 +209,12 @@ Posts.notifyQueuedPostOwner = async (req, res) => { const { id } = req.params; await api.posts.notifyQueuedPostOwner(req, { id, message: req.body.message }); helpers.formatApiResponse(200, res); +}; + +Posts.changeOwner = async (req, res) => { + await api.posts.changeOwner(req, { + pids: req.body.pids || (req.params.pid ? [req.params.pid] : []), + uid: req.body.uid, + }); + helpers.formatApiResponse(200, res); }; \ No newline at end of file diff --git a/src/routes/write/posts.js b/src/routes/write/posts.js index 2c9a54be64..ed2c372461 100644 --- a/src/routes/write/posts.js +++ b/src/routes/write/posts.js @@ -46,6 +46,8 @@ module.exports = function () { setupApiRoute(router, 'put', '/queue/:id', controllers.write.posts.editQueuedPost); setupApiRoute(router, 'post', '/queue/:id/notify', [middleware.checkRequired.bind(null, ['message'])], controllers.write.posts.notifyQueuedPostOwner); + setupApiRoute(router, 'put', '/:pid/owner', [middleware.ensureLoggedIn, middleware.assert.post, middleware.checkRequired.bind(null, ['uid'])], controllers.write.posts.changeOwner); + setupApiRoute(router, 'post', '/owner', [middleware.ensureLoggedIn, middleware.checkRequired.bind(null, ['pids', 'uid'])], controllers.write.posts.changeOwner); // Shorthand route to access post routes by topic index router.all('/+byIndex/:index*?', [middleware.checkRequired.bind(null, ['tid'])], controllers.write.posts.redirectByIndex); diff --git a/src/socket.io/posts/tools.js b/src/socket.io/posts/tools.js index 397b6ef2a4..99a09580bd 100644 --- a/src/socket.io/posts/tools.js +++ b/src/socket.io/posts/tools.js @@ -5,12 +5,13 @@ const nconf = require('nconf'); const db = require('../../database'); const posts = require('../../posts'); const flags = require('../../flags'); -const events = require('../../events'); const privileges = require('../../privileges'); const plugins = require('../../plugins'); const social = require('../../social'); const user = require('../../user'); const utils = require('../../utils'); +const sockets = require('../index'); +const api = require('../../api'); module.exports = function (SocketPosts) { SocketPosts.loadPostTools = async function (socket, data) { @@ -77,23 +78,8 @@ module.exports = function (SocketPosts) { if (!data || !Array.isArray(data.pids) || !data.toUid) { throw new Error('[[error:invalid-data]]'); } - const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(socket.uid); - if (!isAdminOrGlobalMod) { - throw new Error('[[error:no-privileges]]'); - } - - const postData = await posts.changeOwner(data.pids, data.toUid); - const logs = postData.map(({ pid, uid, cid }) => (events.log({ - type: 'post-change-owner', - uid: socket.uid, - ip: socket.ip, - targetUid: data.toUid, - pid: pid, - originalUid: uid, - cid: cid, - }))); - - await Promise.all(logs); + sockets.warnDeprecated(socket, 'PUT /api/v3/posts/owner'); + await api.posts.changeOwner(socket, { pids: data.pids, uid: data.toUid }); }; SocketPosts.getEditors = async function (socket, data) { diff --git a/test/posts.js b/test/posts.js index 2ba85734d4..84befcbfa5 100644 --- a/test/posts.js +++ b/test/posts.js @@ -118,7 +118,7 @@ describe('Post\'s', () => { it('should fail to change owner if user is not authorized', async () => { try { - await socketPosts.changeOwner({ uid: voterUid }, { pids: [1, 2], toUid: voterUid }); + await apiPosts.changeOwner({ uid: voterUid }, { pids: [1, 2], uid: voterUid }); } catch (err) { assert.strictEqual(err.message, '[[error:no-privileges]]'); }