mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
✨ 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:
parent
6920c03b3f
commit
5ba3f5efcf
2 changed files with 111 additions and 24 deletions
|
@ -295,11 +295,13 @@ module.exports.extendModel = function extendModel(Post, Posts, ghostBookshelf) {
|
||||||
}, {
|
}, {
|
||||||
/**
|
/**
|
||||||
* ### destroyByAuthor
|
* ### 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 options = this.filterOptions(unfilteredOptions, 'destroyByAuthor', {extraAllowedProperties: ['id']});
|
||||||
let postCollection = Posts.forge();
|
|
||||||
let authorId = options.id;
|
let authorId = options.id;
|
||||||
|
|
||||||
if (!authorId) {
|
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.
|
const reassignPost = (async () => {
|
||||||
// `posts_authors` are automatically removed (bookshelf-relations)
|
let trx = options.transacting;
|
||||||
// CASE: if you are the secondary author of a post, you are just deleted as author.
|
let knex = ghostBookshelf.knex;
|
||||||
// must happen manually
|
|
||||||
const destroyPost = (() => {
|
try {
|
||||||
return postCollection
|
// There's only one possible owner per Ghost instance
|
||||||
.query('where', 'author_id', '=', authorId)
|
const ownerUser = await knex('roles')
|
||||||
.fetch(options)
|
.transacting(trx)
|
||||||
.call('invokeThen', 'destroy', options)
|
.join('roles_users', 'roles.id', '=', 'roles_users.role_id')
|
||||||
.then(function (response) {
|
.where('roles.name', 'Owner')
|
||||||
return (options.transacting || ghostBookshelf.knex)('posts_authors')
|
.select('roles_users.user_id');
|
||||||
.where('author_id', authorId)
|
const ownerId = ownerUser[0].user_id;
|
||||||
.del()
|
|
||||||
.then(() => response);
|
const authorsPosts = await knex('posts_authors')
|
||||||
})
|
.transacting(trx)
|
||||||
.catch((err) => {
|
.where('author_id', authorId)
|
||||||
throw new errors.InternalServerError({err: err});
|
.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) {
|
if (!options.transacting) {
|
||||||
return ghostBookshelf.transaction((transacting) => {
|
return ghostBookshelf.transaction((transacting) => {
|
||||||
options.transacting = transacting;
|
options.transacting = transacting;
|
||||||
return destroyPost();
|
return reassignPost();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return destroyPost();
|
return reassignPost();
|
||||||
},
|
},
|
||||||
|
|
||||||
permissible: function permissible(postModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) {
|
permissible: function permissible(postModelOrId, action, context, unsafeAttrs, loadedPermissions, hasUserPermission, hasApiKeyPermission) {
|
||||||
|
|
|
@ -196,8 +196,9 @@ describe('User API', function () {
|
||||||
.expect(200);
|
.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 userId = testUtils.getExistingData().users[1].id;
|
||||||
|
const ownerId = testUtils.getExistingData().users[0].id;
|
||||||
|
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(localUtils.API.getApiQuery(`posts/?filter=author_id:${userId}`))
|
.get(localUtils.API.getApiQuery(`posts/?filter=author_id:${userId}`))
|
||||||
|
@ -206,6 +207,24 @@ describe('User API', function () {
|
||||||
|
|
||||||
res.body.posts.length.should.eql(7);
|
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
|
const res2 = await request
|
||||||
.delete(localUtils.API.getApiQuery(`users/${userId}`))
|
.delete(localUtils.API.getApiQuery(`users/${userId}`))
|
||||||
.set('Origin', config.get('url'))
|
.set('Origin', config.get('url'))
|
||||||
|
@ -240,6 +259,22 @@ describe('User API', function () {
|
||||||
|
|
||||||
const rolesUsers = await db.knex('roles_users').select();
|
const rolesUsers = await db.knex('roles_users').select();
|
||||||
rolesUsers.length.should.greaterThan(0);
|
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 () {
|
it('Can transfer ownership to admin user', async function () {
|
||||||
|
|
Loading…
Add table
Reference in a new issue