#!/usr/bin/env node /** * Module dependencies. */ const util = require('./util'); const program = require('commander'); const inquirer = require('inquirer'); const { graphql } = require('graphql'); const helpers = require('../services/migration/helpers'); const { stripIndent } = require('common-tags'); const Table = require('cli-table'); // Make things colorful! require('colors'); // Register the autocomplete plugin. inquirer.registerPrompt( 'autocomplete', require('inquirer-autocomplete-prompt') ); const schema = require('../graph/schema'); const Context = require('../graph/context'); const UsersService = require('../services/users'); const UserModel = require('../models/user'); const CommentModel = require('../models/comment'); const ActionModel = require('../models/action'); const USER_ROLES = require('../models/enum/user_roles'); const mongoose = require('../services/mongoose'); // Register the shutdown criteria. util.onshutdown([() => mongoose.disconnect()]); /** * transforms a specific action to a removal action on the target model. */ const actionDecrTransformer = ({ item_id, action_type, group_id }) => ({ query: { id: item_id }, update: { $inc: { [`action_counts.${action_type.toLowerCase()}`]: -1, [`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1, }, }, }); /** * Deletes a user and cleans up their associated verifications. */ async function deleteUser(userID) { try { // Find the user we're removing. const user = await UserModel.findOne({ id: userID }); if (!user) { throw new Error(`user with id ${userID} not found`); } printUserAsTable(user); console.warn(stripIndent` This will delete the above user. This might take a long time if there is a lot of data, please confirm that you want to continue. `); const { confirm } = await inquirer.prompt({ type: 'confirm', name: 'confirm', message: 'Continue', default: false, }); if (!confirm) { return util.shutdown(); } const { transformSingleWithCursor } = helpers({ queryBatchSize: 10000, updateBatchSize: 10000, }); console.warn("Removing user's actions"); // Remove all actions against comments. await transformSingleWithCursor( ActionModel.collection.find({ user_id: user.id, item_type: 'COMMENTS' }), actionDecrTransformer, CommentModel ); // Remove all actions against users. await transformSingleWithCursor( ActionModel.collection.find({ user_id: user.id, item_type: 'USERS' }), actionDecrTransformer, UserModel ); // Remove all the user's actions. await ActionModel.where({ user_id: user.id }) .setOptions({ multi: true }) .remove(); console.warn("Removing user's comments"); // Removes all the user's reply counts on each of the comments that they // have commented on. await transformSingleWithCursor( CommentModel.collection.aggregate([ { $match: { author_id: user.id } }, { $group: { _id: '$parent_id', count: { $sum: 1 }, }, }, ]), ({ _id: parent_id, count }) => ({ query: { id: parent_id }, update: { $inc: { reply_count: -1 * count, }, }, }), CommentModel ); // Remove all the user's comments. await CommentModel.where({ author_id: user.id }) .setOptions({ multi: true }) .remove(); console.warn('Removing the user'); // Remove the user. await user.remove(); util.shutdown(); } catch (err) { console.error(err); util.shutdown(1); } } function printUserAsTable(user) { let table = new Table({}); table.push( { ID: user.id.gray }, { Username: user.username }, { Emails: user.emails }, { Tags: user.tags ? user.tags.map(({ tag: { name } }) => name) : '' }, { Role: user.role }, { Verified: user.hasVerifiedEmail }, { Username: user.status.username.status }, { Banned: user.banned }, { Suspension: user.suspended ? `Until ${user.status.suspension.until}` : false, } ); console.log(table.toString()); } /** * Searches for users based on their username and email address. */ async function searchUsers() { const ctx = Context.forSystem(); const searchQuery = ` query SearchUsers($value: String) { users(query: {value: $value}) { nodes { id username role profiles { id provider } } } } `; try { const answers = await inquirer.prompt({ type: 'autocomplete', name: 'userID', message: 'Search for a user', source: async (answers, value) => { if (value === null) { value = ''; } const { data, errors } = await graphql(schema, searchQuery, {}, ctx, { value, }); if (errors && errors.length > 0) { throw errors[0]; } if (data.users === null) { return []; } return data.users.nodes.map(user => { const emails = user.profiles .filter(({ provider }) => provider === 'local') .map(({ id }) => id) .join(', '); return { name: `${user.username} (${emails}) ${user.id.gray} - ${ user.role.gray }`, value: user.id, }; }); }, }); const { userID } = answers; const user = await UserModel.findOne({ id: userID }); printUserAsTable(user); util.shutdown(0); } catch (err) { console.error(err); util.shutdown(1); } } /** * Adds a role to a user * @param {String} userUD id of the user to add the role to * @param {String} role the role to add */ async function setUserRole(userID) { try { const { role } = await inquirer.prompt([ { name: 'role', message: 'User Role', type: 'list', choices: USER_ROLES, }, ]); await UsersService.setRole(userID, role); console.log(`Set User ${userID} to the ${role} role.`); util.shutdown(); } catch (err) { console.error(err); util.shutdown(1); } } /** * Verifies an email address for a user. * * @param userID the user's id * @param email the user's email address to be verified, otherwise verifies the * first email if there is one, if there are multiple, you get a * prompt. */ async function verifyUserEmail(userID, email) { try { // Get the user. const user = await UserModel.findOne({ id: userID }); if (!user) { throw new Error(`user with ID ${userID} cannot be found`); } // Get all the user's email addresses. const emails = user.emails; if (emails.length === 0) { throw new Error('user did not have any email addresses'); } if (!email && emails.length === 1) { // The email wasn't passed, and there is only one option. email = emails[0]; } else if (!emails.includes(email)) { // The email passed doesn't belong to this user. throw new Error(`user does not have the email ${email}`); } else if (emails.length > 1) { // The email wasn't passed, and there is more than one choice. const answers = await inquirer.prompt([ { name: 'email', message: 'Select Email to Verify', type: 'list', choices: emails, }, ]); email = answers.email; } // Verify the email. await UsersService.confirmEmail(userID, email); console.log(`User ${userID} had their email ${email} verified.`); util.shutdown(); } catch (err) { console.error(err); util.shutdown(1); } } //============================================================================== // Setting up the program command line arguments. //============================================================================== program .command('delete ') .description('delete a user') .action(deleteUser); program .command('list') .description('searches for a user based on their stored username and email') .action(searchUsers); program .command('set-role ') .description('sets the role on a user') .action(setUserRole); program .command('verify ') .description("verifies the given user's email address") .action(verifyUserEmail); program.parse(process.argv); // If there is no command listed, output help. if (!process.argv.slice(2).length) { program.outputHelp(); util.shutdown(); }