216 lines
7.1 KiB
JavaScript
216 lines
7.1 KiB
JavaScript
// @ts-check
|
|
const path = require('path');
|
|
const ObjectId = require('bson-objectid').default;
|
|
|
|
/**
|
|
* @TODO: pass these in as dependencies
|
|
*/
|
|
const {PostRevisions} = require('@tryghost/post-revisions');
|
|
const DomainEvents = require('@tryghost/domain-events/lib/DomainEvents');
|
|
const {PostsBulkAddTagsEvent} = require('@tryghost/post-events');
|
|
|
|
/**
|
|
* @typedef {Object} IdbBackup
|
|
* @prop {() => Promise<string>} backup
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} IModels
|
|
* @prop {Object} Base
|
|
* @prop {(callback: function) => Promise} Base.transaction
|
|
* @prop {Object} Post
|
|
* @prop {(frameOptions: Object) => Promise} Post.reassignByAuthor
|
|
* @prop {Object} ApiKey
|
|
* @prop {(Object) => Promise} ApiKey.destroy
|
|
* @prop {Object} ApiKey.NotFoundError
|
|
* @prop {Object} User
|
|
* @prop {(Object) => Promise} User.destroy
|
|
* @prop {(Object) => Promise} User.findAll
|
|
* @prop {Object} Session
|
|
* @prop {(Object) => Promise} Session.findAll
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} IAuth
|
|
* @prop {Object} setup
|
|
* @prop {(isComplete: boolean) => () => Promise} setup.assertSetupCompleted
|
|
* @prop {Object} passwordreset
|
|
* @prop {(email: string, apiSettings: Object, transcation?: Object) => Promise<string>} passwordreset.generateToken
|
|
* @prop {(token: string, apiMail: Object) => Promise} passwordreset.sendResetNotification
|
|
*/
|
|
|
|
class Users {
|
|
/**
|
|
* @param {Object} dependencies
|
|
* @param {IdbBackup} dependencies.dbBackup
|
|
* @param {IModels} dependencies.models
|
|
* @param {IAuth} dependencies.auth
|
|
* @param {Object} dependencies.apiMail
|
|
* @param {Object} dependencies.apiSettings
|
|
*/
|
|
constructor({dbBackup, models, auth, apiMail, apiSettings}) {
|
|
this.dbBackup = dbBackup;
|
|
this.models = models;
|
|
this.auth = auth;
|
|
this.apiMail = apiMail;
|
|
this.apiSettings = apiSettings;
|
|
|
|
this.assignTagToUserPosts = this.assignTagToUserPosts.bind(this);
|
|
}
|
|
|
|
async resetAllPasswords(frameOptions) {
|
|
return this.models.Base.transaction(async (t) => {
|
|
frameOptions.transacting = t;
|
|
|
|
// Reset all passwords
|
|
const users = await this.models.User.findAll(frameOptions);
|
|
for (const user of users) {
|
|
await user.save({
|
|
status: 'locked' // Prevent signins before password reset
|
|
}, frameOptions);
|
|
}
|
|
|
|
//Send all password resets
|
|
for (const user of users) {
|
|
const token = await this.auth.passwordreset.generateToken(user.get('email'), this.apiSettings, t);
|
|
await this.auth.passwordreset.sendResetNotification(token, this.apiMail);
|
|
}
|
|
});
|
|
}
|
|
|
|
async assignTagToUserPosts({id, context, transacting}) {
|
|
// get author slug
|
|
const author = await this.models.User.findOne({
|
|
id
|
|
}, {
|
|
id,
|
|
context,
|
|
transacting
|
|
});
|
|
|
|
// get list of posts that need the tag assigned
|
|
const userPosts = await this.models.Base.knex('posts_authors')
|
|
.transacting(transacting)
|
|
.where('author_id', id)
|
|
.select('post_id');
|
|
let usersPostIds = userPosts.map(p => p.post_id);
|
|
|
|
if (usersPostIds.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// create an internal tag to assign to reassigned posts
|
|
// in following format: `#{author_slug}`
|
|
let createdTag = false;
|
|
let tag = await this.models.Tag.findOne({
|
|
slug: `hash-${author.get('slug')}`
|
|
}, {
|
|
context,
|
|
transacting
|
|
});
|
|
if (!tag) {
|
|
tag = await this.models.Tag.add({
|
|
slug: `#${author.get('slug')}`
|
|
}, {
|
|
context,
|
|
transacting
|
|
});
|
|
createdTag = true;
|
|
}
|
|
|
|
// filter out posts that already have the tag if we didn't need to create one
|
|
if (!createdTag) {
|
|
const tagId = tag.get('id');
|
|
const taggedPostIds = await this.models.Base.knex('posts_tags')
|
|
.transacting(transacting)
|
|
.where('tag_id', tagId)
|
|
.select('post_id');
|
|
usersPostIds = userPosts
|
|
.map(p => p.post_id)
|
|
.filter(p => !taggedPostIds.includes(p));
|
|
}
|
|
|
|
// assign tag to posts
|
|
// do bulk insert for performance reasons
|
|
// - go ahead and assign sort_order 0 to all of them
|
|
await this.models.Base.knex('posts_tags')
|
|
.transacting(transacting)
|
|
.insert(usersPostIds.map(postId => ({
|
|
id: (new ObjectId()).toHexString(),
|
|
post_id: postId,
|
|
tag_id: tag.get('id'),
|
|
sort_order: 0
|
|
})));
|
|
|
|
// manually add an entry in the Actions table that specifies the number of posts edited; see #bulkAddTags for similar logic
|
|
await this.models.Post.addActions('edited', usersPostIds, {transacting, context});
|
|
|
|
// dispatch event to ensure collections are updated
|
|
DomainEvents.dispatch(PostsBulkAddTagsEvent.create(usersPostIds));
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Object} frameOptions
|
|
* @param {string} frameOptions.id - user ID to destroy
|
|
* @param {Object} frameOptions.context - frame context to perform the action
|
|
* @returns
|
|
*/
|
|
async destroyUser(frameOptions) {
|
|
let filename = null;
|
|
const backupPath = await this.dbBackup.backup();
|
|
|
|
if (backupPath) {
|
|
const parsedFileName = path.parse(backupPath);
|
|
filename = `${parsedFileName.name}${parsedFileName.ext}`;
|
|
}
|
|
|
|
return this.models.Base.transaction(async (t) => {
|
|
frameOptions.transacting = t;
|
|
|
|
// null author field for users' post revisions
|
|
const postRevisions = new PostRevisions({
|
|
model: this.models.PostRevision
|
|
});
|
|
await postRevisions.removeAuthorFromRevisions(frameOptions.id, {
|
|
transacting: frameOptions.transacting
|
|
});
|
|
|
|
// create a #author-slug tag and assign it to their posts
|
|
await this.assignTagToUserPosts({
|
|
id: frameOptions.id,
|
|
context: frameOptions.context,
|
|
transacting: frameOptions.transacting
|
|
});
|
|
|
|
// reassign posts to owner user
|
|
await this.models.Post.reassignByAuthor({
|
|
id: frameOptions.id,
|
|
context: frameOptions.context,
|
|
transacting: frameOptions.transacting
|
|
});
|
|
|
|
// delete user
|
|
try {
|
|
await this.models.ApiKey.destroy({
|
|
...frameOptions,
|
|
require: true,
|
|
destroyBy: {
|
|
user_id: frameOptions.id
|
|
}
|
|
});
|
|
} catch (err) {
|
|
if (!(err instanceof this.models.ApiKey.NotFoundError)) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
await this.models.User.destroy(Object.assign({status: 'all'}, frameOptions));
|
|
|
|
return filename;
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = Users;
|