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
This commit is contained in:
Barış Uşaklı
2026-01-11 14:38:14 -05:00
committed by Barış Soner Uşaklı
parent b0679cadcf
commit 00b9ca111e
9 changed files with 125 additions and 25 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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() {

View File

@@ -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);
};

View File

@@ -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);
};

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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]]');
}