Reassigned posts when deleting a user

refs https://github.com/TryGhost/Toolbox/issues/268

 - When the user is removed our current pattern was deleting their posts. This didn't work well and created all sorts of problems
 - As a solution we now reassign any posts that are only authored by the deleted user to the owner user
 - This change also reduced the dependency on "author" field
This commit is contained in:
Naz 2022-04-26 14:33:02 +08:00 committed by Daniel Lockyer
parent 6920c03b3f
commit 5ba3f5efcf
No known key found for this signature in database
GPG Key ID: D21186F0B47295AD
2 changed files with 111 additions and 24 deletions

View File

@ -295,11 +295,13 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
}, {
/**
* ### destroyByAuthor
* @param {[type]} options has context and id. Context is the user doing the destroy, id is the user to destroy
* @param {Object} unfilteredOptions has context and id. Context is the user doing the destroy, id is the user to destroy
* @param {string} unfilteredOptions.id
* @param {Object} unfilteredOptions.context
* @param {Object} unfilteredOptions.transacting
*/
destroyByAuthor: function destroyByAuthor(unfilteredOptions) {
destroyByAuthor: async function destroyByAuthor(unfilteredOptions) {
let options = this.filterOptions(unfilteredOptions, 'destroyByAuthor', {extraAllowedProperties: ['id']});
let postCollection = Posts.forge();
let authorId = options.id;
if (!authorId) {
@ -308,34 +310,84 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
}));
}
// CASE: if you are the primary author of a post, the whole post and it's relations get's deleted.
// `posts_authors` are automatically removed (bookshelf-relations)
// CASE: if you are the secondary author of a post, you are just deleted as author.
// must happen manually
const destroyPost = (() => {
return postCollection
.query('where', 'author_id', '=', authorId)
.fetch(options)
.call('invokeThen', 'destroy', options)
.then(function (response) {
return (options.transacting || ghostBookshelf.knex)('posts_authors')
.where('author_id', authorId)
.del()
.then(() => response);
})
.catch((err) => {
throw new errors.InternalServerError({err: err});
});
const reassignPost = (async () => {
let trx = options.transacting;
let knex = ghostBookshelf.knex;
try {
// There's only one possible owner per Ghost instance
const ownerUser = await knex('roles')
.transacting(trx)
.join('roles_users', 'roles.id', '=', 'roles_users.role_id')
.where('roles.name', 'Owner')
.select('roles_users.user_id');
const ownerId = ownerUser[0].user_id;
const authorsPosts = await knex('posts_authors')
.transacting(trx)
.where('author_id', authorId)
.select('post_id', 'sort_order');
const ownersPosts = await knex('posts_authors')
.transacting(trx)
.where('author_id', ownerId)
.select('post_id');
const authorsPrimaryPosts = authorsPosts.filter(ap => ap.sort_order === 0);
const primaryPostsWithOwnerCoauthor = _.intersectionBy(authorsPrimaryPosts, ownersPosts, 'post_id');
const primaryPostsWithOwnerCoauthorIds = primaryPostsWithOwnerCoauthor.map(post => post.post_id);
// remove author and bump owner's sort_order to 0 to make them a primary author
// remove author from posts
await knex('posts_authors')
.transacting(trx)
.whereIn('post_id', primaryPostsWithOwnerCoauthorIds)
.where('author_id', authorId)
.del();
// make the owner a primary author
await knex('posts_authors')
.transacting(trx)
.whereIn('post_id', primaryPostsWithOwnerCoauthorIds)
.where('author_id', ownerId)
.update('sort_order', 0);
const primaryPostsWithoutOwnerCoauthor = _.differenceBy(authorsPrimaryPosts, primaryPostsWithOwnerCoauthor, 'post_id');
const postsWithoutOwnerCoauthorIds = primaryPostsWithoutOwnerCoauthor.map(post => post.post_id);
// swap out current author with the owner
await knex('posts_authors')
.transacting(trx)
.whereIn('post_id', postsWithoutOwnerCoauthorIds)
.where('author_id', authorId)
.update('author_id', ownerId);
// remove author as secondary author from any other posts
await knex('posts_authors')
.transacting(trx)
.where('author_id', authorId)
.del();
// --------- secondary author cleanup END
// make the owner a primary author in post table
// remove this statement once 'author' concept is gone
await knex('posts')
.transacting(trx)
.where('author_id', authorId)
.update('author_id', ownerId);
} catch (err) {
throw new errors.InternalServerError({err: err});
}
});
if (!options.transacting) {
return ghostBookshelf.transaction((transacting) => {
options.transacting = transacting;
return destroyPost();
return reassignPost();
});
}
return destroyPost();
return reassignPost();
},
permissible: function permissible(postModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) {

View File

@ -196,8 +196,9 @@ describe('User API', function () {
.expect(200);
});
it('Can destroy an active user', async function () {
it('Can destroy an active user and transfer posts to the owner', async function () {
const userId = testUtils.getExistingData().users[1].id;
const ownerId = testUtils.getExistingData().users[0].id;
const res = await request
.get(localUtils.API.getApiQuery(`posts/?filter=author_id:${userId}`))
@ -206,6 +207,24 @@ describe('User API', function () {
res.body.posts.length.should.eql(7);
const ownerPostsAuthorsModels = await db.knex('posts_authors')
.where({
author_id: ownerId
})
.select();
// includes posts & pages
should.equal(ownerPostsAuthorsModels.length, 8);
const userPostsAuthorsModels = await db.knex('posts_authors')
.where({
author_id: userId
})
.select();
// includes posts & pages
should.equal(userPostsAuthorsModels.length, 11);
const res2 = await request
.delete(localUtils.API.getApiQuery(`users/${userId}`))
.set('Origin', config.get('url'))
@ -240,6 +259,22 @@ describe('User API', function () {
const rolesUsers = await db.knex('roles_users').select();
rolesUsers.length.should.greaterThan(0);
const ownerPostsAuthorsModelsAfter = await db.knex('posts_authors')
.where({
author_id: ownerId
})
.select();
should.equal(ownerPostsAuthorsModelsAfter.length, 19);
const userPostsAuthorsModelsAfter = await db.knex('posts_authors')
.where({
author_id: userId
})
.select();
should.equal(userPostsAuthorsModelsAfter.length, 0);
});
it('Can transfer ownership to admin user', async function () {