diff --git a/bin/cli b/bin/cli index 002288648..5b52d96b9 100755 --- a/bin/cli +++ b/bin/cli @@ -4,45 +4,13 @@ * Module dependencies. */ -const program = require('commander'); -const pkg = require('../package.json'); -const dotenv = require('dotenv'); const util = require('../util'); - -//============================================================================== -// Setting up the program command line arguments. -//============================================================================== - -program - .version(pkg.version) - .option('-c, --config [path]', 'Specify the configuration file to load') - .option('--pid [path]', 'Specify a path to output the current PID to') - .parse(process.argv); - -if (program.config) { - let r = dotenv.config({ - path: program.config - }); - - if (r.error) { - throw r.error; - } -} - -// If the `--pid` flag is used, put the current pid there. -if (program.pid) { - util.pid(program.pid); -} - -// Perform rewrites to the runtime environment variables based on the contents -// of the process.env.REWRITE_ENV if it exists. This is done here as it is the -// entrypoint for the entire application. -require('env-rewrite').rewrite(); +const program = require('./commander'); program .command('serve', 'serve the application') .command('assets', 'interact with assets') - .command('settings', 'work with the application settings') + .command('setup', 'setup the application') .command('jobs', 'work with the job queues') .command('users', 'work with the application auth') .parse(process.argv); diff --git a/bin/cli-assets b/bin/cli-assets index f7d6a0bbb..76ca5735b 100755 --- a/bin/cli-assets +++ b/bin/cli-assets @@ -4,8 +4,7 @@ * Module dependencies. */ -const program = require('commander'); -const pkg = require('../package.json'); +const program = require('./commander'); const parseDuration = require('parse-duration'); const Table = require('cli-table'); const AssetModel = require('../models/asset'); @@ -86,9 +85,6 @@ function refreshAssets(ageString) { // Setting up the program command line arguments. //============================================================================== -program - .version(pkg.version); - program .command('list') .description('list all the assets in the database') diff --git a/bin/cli-jobs b/bin/cli-jobs index 879927197..4d348fabb 100755 --- a/bin/cli-jobs +++ b/bin/cli-jobs @@ -4,7 +4,7 @@ * Module dependencies. */ -const program = require('commander'); +const program = require('./commander'); const scraper = require('../services/scraper'); const mailer = require('../services/mailer'); const util = require('../util'); diff --git a/bin/cli-serve b/bin/cli-serve index c8cf37a47..d861d8c5d 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -1,6 +1,7 @@ #!/usr/bin/env node const app = require('../app'); +const program = require('./commander'); const debug = require('debug')('talk:server'); const http = require('http'); const init = require('../init'); @@ -101,12 +102,6 @@ function startApp() { }); } -/** - * Module dependencies. - */ - -const program = require('commander'); - //============================================================================== // Setting up the program command line arguments. //============================================================================== diff --git a/bin/cli-settings b/bin/cli-settings deleted file mode 100755 index ba4305ce3..000000000 --- a/bin/cli-settings +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env node - -/** - * Module dependencies. - */ - -const program = require('commander'); -const mongoose = require('../services/mongoose'); -const SettingsService = require('../services/settings'); -const util = require('../util'); - -// Register the shutdown criteria. -util.onshutdown([ - () => mongoose.disconnect() -]); - -//============================================================================== -// Setting up the program command line arguments. -//============================================================================== - -program - .command('init') - .description('initilizes the talk settings') - .action(() => { - const defaults = { - moderation: 'POST', - wordlist: { - banned: [], - suspect: [] - } - }; - - SettingsService - .init(defaults) - .then(() => { - console.log('Created settings object.'); - util.shutdown(); - }) - .catch((err) => { - console.error(`failed to create the settings object ${JSON.stringify(err)}`); - util.shutdown(1); - }); - }); - -program.parse(process.argv); - -// If there is no command listed, output help. -if (!process.argv.slice(2).length) { - program.outputHelp(); - util.shutdown(); -} diff --git a/bin/cli-setup b/bin/cli-setup new file mode 100755 index 000000000..1c348d212 --- /dev/null +++ b/bin/cli-setup @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +const program = require('./commander'); +const inquirer = require('inquirer'); +const mongoose = require('../services/mongoose'); +const SettingModel = require('../models/setting'); +const SettingsService = require('../services/settings'); +const util = require('../util'); + +// Register the shutdown criteria. +util.onshutdown([ + () => mongoose.disconnect() +]); + +//============================================================================== +// Setting up the program command line arguments. +//============================================================================== + +program + .description('runs the setup wizard to setup the application') + .parse(process.argv); + +//============================================================================== +// Setup the application +//============================================================================== + +console.log('We\'ll ask you some questions in order to setup your installation of Talk.\n'); + +SettingsService + .retrieve() + .catch(() => new SettingModel()) + .then((settings) => { + + if (settings && settings.id) { + console.log('We found preexisting settings in the database, we\'ll use it\'s values as defaults.\n'); + } + + return inquirer.prompt([ + { + type: 'input', + name: 'organizationName', + message: 'Organization Name', + default: settings.organizationName + }, + { + type: 'list', + choices: SettingModel.MODERATION_OPTIONS, + name: 'moderation', + default: settings.moderation, + message: 'Select a moderation mode' + }, + { + type: 'confirm', + name: 'requireEmailConfirmation', + default: settings.requireEmailConfirmation, + message: 'Should emails always be confirmed' + } + ]) + .then((answers) => { + + // Update the settings that were changed. + Object.keys(answers).forEach((key) => { + if (answers[key] !== undefined) { + settings[key] = answers[key]; + } + }); + + return SettingsService.update(settings); + }); + }) + .then(() => { + console.log('\nTalk is now installed!'); + util.shutdown(); + }) + .catch((err) => { + console.error(err); + util.shutdown(1); + }); diff --git a/bin/cli-users b/bin/cli-users index a7e9cdbda..3ce418927 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -4,105 +4,113 @@ * Module dependencies. */ -const program = require('commander'); -const pkg = require('../package.json'); -const prompt = require('prompt'); +const program = require('./commander'); +const inquirer = require('inquirer'); const UsersService = require('../services/users'); const UserModel = require('../models/user'); const mongoose = require('../services/mongoose'); const util = require('../util'); const Table = require('cli-table'); +const validateRequired = (msg = 'Field is required') => (input) => { + if (input && input.length > 0) { + return true; + } + + return msg; +}; + // Regeister the shutdown criteria. util.onshutdown([ () => mongoose.disconnect() ]); +function getUserCreateAnswers(options) { + if (options.flag_mode) { + + if (options.role && UserModel.USER_ROLES.indexOf(options.role) > -1) { + + let roles = {}; + roles[options.role] = true; + + return Promise.resolve({ + email: options.email, + password: options.password, + displayName: options.name, + roles + }); + } + } + + return inquirer.prompt([ + { + name: 'email', + message: 'Email', + format: 'email', + validate: validateRequired('Email is required') + }, + { + name: 'password', + message: 'Password', + type: 'password', + validate: validateRequired('Password is required') + }, + { + name: 'confirmPassword', + message: 'Confirm Password', + type: 'password', + validate: validateRequired('Confirm Password is required') + }, + { + name: 'displayName', + message: 'Display Name', + validate: validateRequired('Display Name is required') + }, + { + name: 'roles', + message: 'User Role', + type: 'checkbox', + choices: UserModel.USER_ROLES + } + ]); +} + /** * Prompts for input and registers a user based on those. */ function createUser(options) { - return new Promise((resolve, reject) => { - - if (options.flag_mode) { - return resolve({ - email: options.email, - password: options.password, - displayName: options.name, - role: options.role - }); - } - - prompt.start(); - - prompt.get([ - { - name: 'email', - description: 'Email', - format: 'email', - required: true - }, - { - name: 'password', - description: 'Password', - hidden: true, - required: true - }, - { - name: 'confirmPassword', - description: 'Confirm Password', - hidden: true, - required: true - }, - { - name: 'displayName', - description: 'Display Name', - required: true - }, - { - name: 'role', - description: 'User Role', - required: false - } - ], (err, result) => { - if (err) { - return reject(err); + getUserCreateAnswers(options) + .then((answers) => { + if (answers.password !== answers.confirmPassword) { + return Promise.reject(new Error('Passwords do not match')); } - if (result.password !== result.confirmPassword) { - return reject(new Error('Passwords do not match')); - } + return answers; + }) + .then((answers) => { + return UsersService + .createLocalUser(answers.email.trim(), answers.password.trim(), answers.displayName.trim()) + .then((user) => { + console.log(`Created user ${user.id}.`); - resolve(result); - }); - }) - .then((result) => { - return UsersService.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim()) - .then((user) => { - console.log(`Created user ${user.id}.`); - - let role = result.role ? result.role.trim() : null; - - if (role && role.length > 0) { - return UsersService - .addRoleToUser(user.id, role) - .then(() => { - console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(); - }); - } else { - util.shutdown(); - } + if (answers.roles.length > 0) { + return Promise.all(answers.roles.map((role) => { + return UsersService + .addRoleToUser(user.id, role) + .then(() => { + console.log(`Added the role ${role} to User ${user.id}.`); + }); + })); + } + }); + }) + .then(() => { + util.shutdown(); }) .catch((err) => { console.error(err); util.shutdown(); }); - }); } /** @@ -127,44 +135,34 @@ function deleteUser(userID) { * Changes the password for a user. */ function passwd(userID) { - prompt.start(); - - prompt.get([ + inquirer.prompt([ { name: 'password', - description: 'Password', - hidden: true, - required: true + message: 'Password', + type: 'password', + validate: validateRequired('Password is required') }, { name: 'confirmPassword', - description: 'Confirm Password', - hidden: true, - required: true + message: 'Confirm Password', + type: 'password', + validate: validateRequired('Confirm Password is required') } - ], (err, result) => { - if (err) { - console.error(err); - util.shutdown(); - return; + ]) + .then((answers) => { + if (answers.password !== answers.confirmPassword) { + return Promise.reject(new Error('Password mismatch')); } - if (result.password !== result.confirmPassword) { - console.error(new Error('Password mismatch')); - util.shutdown(1); - return; - } - - UsersService - .changePassword(userID, result.password) - .then(() => { - console.log('Password changed.'); - util.shutdown(); - }) - .catch((err) => { - console.error(err); - util.shutdown(1); - }); + return UsersService.changePassword(userID, answers.password); + }) + .then(() => { + console.log('Password changed.'); + util.shutdown(); + }) + .catch((err) => { + console.error(err); + util.shutdown(1); }); } @@ -273,6 +271,13 @@ function mergeUsers(dstUserID, srcUserID) { * @param {String} role the role to add */ function addRole(userID, role) { + + if (UserModel.USER_ROLES.indexOf(role) === -1) { + console.error(`Role '${role}' is not supported. Supported roles are ${UserModel.USER_ROLES.join(', ')}.`); + util.shutdown(1); + return; + } + UsersService .addRoleToUser(userID, role) .then(() => { @@ -291,6 +296,13 @@ function addRole(userID, role) { * @param {String} role the role to remove */ function removeRole(userID, role) { + + if (UserModel.USER_ROLES.indexOf(role) === -1) { + console.error(`Role '${role}' is not supported. Supported roles are ${UserModel.USER_ROLES.join(', ')}.`); + util.shutdown(1); + return; + } + UsersService .removeRoleFromUser(userID, role) .then(() => { @@ -375,9 +387,6 @@ function enableUser(userID) { // Setting up the program command line arguments. //============================================================================== -program - .version(pkg.version); - program .command('create') .option('--email [email]', 'Email to use') diff --git a/bin/commander.js b/bin/commander.js new file mode 100644 index 000000000..2a3d1e363 --- /dev/null +++ b/bin/commander.js @@ -0,0 +1,46 @@ +const pkg = require('../package.json'); +const dotenv = require('dotenv'); +const fs = require('fs'); +const program = require('commander'); +const util = require('../util'); + +// Perform rewrites to the runtime environment variables based on the contents +// of the process.env.REWRITE_ENV if it exists. This is done here as it is the +// entrypoint for the entire application. +require('env-rewrite').rewrite(); + +//============================================================================== +// Setting up the program command line arguments. +//============================================================================== + +const parseArgs = require('minimist')(process.argv.slice(2), { + alias: { + 'c': 'config' + }, + string: [ + 'config', + 'pid' + ], + default: { + 'config': null, + 'pid': null + } +}); + +if (parseArgs.config) { + let envConfig = dotenv.parse(fs.readFileSync(parseArgs.config, {encoding: 'utf8'})); + + Object.keys(envConfig).forEach((k) => { + process.env[k] = envConfig[k]; + }); +} + +// If the `--pid` flag is used, put the current pid there. +if (parseArgs.pid) { + util.pid(parseArgs.pid); +} + +module.exports = program + .version(pkg.version) + .option('-c, --config [path]', 'Specify the configuration file to load') + .option('--pid [path]', 'Specify a path to output the current PID to'); diff --git a/errors.js b/errors.js index 1b26e7170..bddcf5989 100644 --- a/errors.js +++ b/errors.js @@ -110,10 +110,15 @@ const ErrNotAuthorized = new APIError('not authorized', { status: 401 }); +// ErrSettingsNotInit is returned when the settings are required but not +// initialized. +const ErrSettingsNotInit = new Error('settings not initialized, run `./bin/cli setup` to setup the application first'); + module.exports = { ExtendableError, APIError, ErrPasswordTooShort, + ErrSettingsNotInit, ErrMissingEmail, ErrMissingPassword, ErrMissingToken, diff --git a/models/setting.js b/models/setting.js index 73f1a8da8..26faf1319 100644 --- a/models/setting.js +++ b/models/setting.js @@ -1,6 +1,11 @@ const mongoose = require('../services/mongoose'); const Schema = mongoose.Schema; +const MODERATION_OPTIONS = [ + 'PRE', + 'POST' +]; + const WordlistSchema = new Schema({ banned: [String], suspect: [String] @@ -19,10 +24,7 @@ const SettingSchema = new Schema({ }, moderation: { type: String, - enum: [ - 'PRE', - 'POST' - ], + enum: MODERATION_OPTIONS, default: 'POST' }, infoBoxEnable: { @@ -33,6 +35,9 @@ const SettingSchema = new Schema({ type: String, default: '' }, + organizationName: { + type: String + }, closedTimeout: { type: Number, @@ -87,3 +92,4 @@ SettingSchema.method('merge', function(src) { const Setting = mongoose.model('Setting', SettingSchema); module.exports = Setting; +module.exports.MODERATION_OPTIONS = MODERATION_OPTIONS; diff --git a/package.json b/package.json index 641d85757..07e67c5b8 100644 --- a/package.json +++ b/package.json @@ -68,10 +68,12 @@ "graphql-tag": "^1.2.3", "graphql-tools": "^0.9.0", "helmet": "^3.1.0", + "inquirer": "^3.0.1", "jsonwebtoken": "^7.1.9", "kue": "^0.11.5", "lodash": "^4.16.6", "metascraper": "^1.0.6", + "minimist": "^1.2.0", "mongoose": "^4.6.5", "morgan": "^1.7.0", "natural": "^0.4.0", @@ -80,7 +82,6 @@ "passport": "^0.3.2", "passport-facebook": "^2.1.1", "passport-local": "^1.0.0", - "prompt": "^1.0.0", "react-apollo": "^0.8.1", "redis": "^2.6.3", "uuid": "^2.0.3" diff --git a/services/settings.js b/services/settings.js index d82fb91a5..5d4133fe4 100644 --- a/services/settings.js +++ b/services/settings.js @@ -1,4 +1,5 @@ const SettingModel = require('../models/setting'); +const errors = require('../errors'); /** * The selector used to uniquely identify the settings document. @@ -15,7 +16,15 @@ module.exports = class SettingsService { * @return {Promise} settings the whole settings record */ static retrieve() { - return SettingModel.findOne(selector); + return SettingModel + .findOne(selector) + .then((settings) => { + if (!settings) { + return Promise.reject(errors.ErrSettingsNotInit); + } + + return settings; + }); } /** diff --git a/services/wordlist.js b/services/wordlist.js index a06801e87..dde032f97 100644 --- a/services/wordlist.js +++ b/services/wordlist.js @@ -203,6 +203,7 @@ class Wordlist { */ static displayNameCheck(displayName) { const wl = new Wordlist(); + return wl.load() .then(() => { displayName = displayName.replace(/_/g, ''); diff --git a/util.js b/util.js index 585cfdcb7..ab9035fe5 100644 --- a/util.js +++ b/util.js @@ -20,6 +20,8 @@ util.shutdown = (defaultCode = 0, signal = null) => { debug(`Reached ${signal} signal`); } + debug(`${util.toshutdown.length} jobs now being called`); + Promise .all(util.toshutdown.map((func) => func ? func(signal) : null).filter((func) => func)) .then(() => { @@ -41,7 +43,7 @@ util.shutdown = (defaultCode = 0, signal = null) => { */ util.onshutdown = (jobs) => { - debug(`${jobs.length} jobs registered`); + debug(`${jobs.length} jobs registered to be called during shutdown`); // Add the new jobs to shutdown to the object reference. util.toshutdown = util.toshutdown.concat(jobs);