diff --git a/bin/cli b/bin/cli
index c5c61fbf6..5b52d96b9 100755
--- a/bin/cli
+++ b/bin/cli
@@ -4,38 +4,13 @@
* Module dependencies.
*/
-const program = require('commander');
-const pkg = require('../package.json');
-const dotenv = require('dotenv');
-
-//==============================================================================
-// Setting up the program command line arguments.
-//==============================================================================
-
-program
- .version(pkg.version)
- .option('-c, --config [path]', 'Specify the configuration file to load')
- .parse(process.argv);
-
-if (program.config) {
- let r = dotenv.config({
- path: program.config
- });
-
- if (r.error) {
- throw r.error;
- }
-}
-
-// 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 util = require('../util');
+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);
@@ -43,4 +18,15 @@ program
// If there is no command listed, output help.
if (!process.argv.slice(2).length) {
program.outputHelp();
+ return;
}
+
+// The ensures that the child process that is created here is always cleaned up
+// properly when the parent process dies.
+util.onshutdown([
+ (signal) => {
+ if ((program.runningCommand.killed === false) && (program.runningCommand.exitCode === null)) {
+ program.runningCommand.kill(signal);
+ }
+ }
+]);
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..0427dcf34 100755
--- a/bin/cli-serve
+++ b/bin/cli-serve
@@ -1,9 +1,9 @@
#!/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');
const scraper = require('../services/scraper');
const mailer = require('../services/mailer');
const kue = require('../services/kue');
@@ -86,27 +86,15 @@ function onListening() {
* Start the app.
*/
function startApp() {
- init().then(() => {
- /**
- * Listen on provided port, on all network interfaces.
- */
- server.listen(port);
- server.on('error', onError);
- server.on('listening', onListening);
- })
- .catch((err) => {
- console.error(err);
- util.shutdown(1);
- });
+ /**
+ * Listen on provided port, on all network interfaces.
+ */
+ server.listen(port);
+ server.on('error', onError);
+ server.on('listening', onListening);
}
-/**
- * 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 7854942c0..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: 'PRE',
- 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..eaa2b90b1
--- /dev/null
+++ b/bin/cli-setup
@@ -0,0 +1,88 @@
+#!/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')
+ .option('--defaults', 'apply defaults for config instead of prompting')
+ .parse(process.argv);
+
+//==============================================================================
+// Setup the application
+//==============================================================================
+
+SettingsService
+ .init()
+ .then((settings) => {
+ if (program.defaults) {
+ return settings.save();
+ }
+
+ console.log('We\'ll ask you some questions in order to setup your installation of Talk.\n');
+
+ return inquirer.prompt([
+ {
+ type: 'input',
+ name: 'organizationName',
+ message: 'Organization Name',
+ default: settings.organizationName,
+ validate: (input) => {
+ if (input && input.length > 0) {
+ return true;
+ }
+
+ return 'Organization Name is required.';
+ }
+ },
+ {
+ 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 settings.save();
+ });
+ })
+ .then(() => {
+ console.log('Talk 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..072a143ee 100755
--- a/bin/cli-users
+++ b/bin/cli-users
@@ -4,105 +4,132 @@
* 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', len = 1) => (input) => {
+ if (input && input.length >= len) {
+ return true;
+ }
+
+ return msg;
+};
+
// Regeister the shutdown criteria.
util.onshutdown([
() => mongoose.disconnect()
]);
+function getUserCreateAnswers(options) {
+ if (options.flag_mode) {
+
+ let user = {
+ email: options.email,
+ password: options.password,
+ confirmPassword: options.password,
+ displayName: options.name,
+ roles: []
+ };
+
+ if (options.role && UserModel.USER_ROLES.indexOf(options.role) > -1) {
+ user.roles = [options.role];
+ }
+
+ return Promise.resolve(user);
+ }
+
+ return inquirer.prompt([
+ {
+ name: 'email',
+ message: 'Email',
+ format: 'email',
+ validate: validateRequired('Email is required')
+ },
+ {
+ name: 'password',
+ message: 'Password',
+ type: 'password',
+ filter: (password) => {
+ return UsersService
+ .isValidPassword(password)
+ .catch((err) => {
+ throw err.message;
+ });
+ }
+ },
+ {
+ name: 'confirmPassword',
+ message: 'Confirm Password',
+ type: 'password',
+ filter: (confirmPassword) => {
+ return UsersService
+ .isValidPassword(confirmPassword)
+ .catch((err) => {
+ throw err.message;
+ });
+ }
+ },
+ {
+ name: 'displayName',
+ message: 'Display Name',
+ filter: (displayName) => {
+ return UsersService
+ .isValidDisplayName(displayName)
+ .catch((err) => {
+ throw err.message;
+ });
+ }
+ },
+ {
+ 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 +154,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 +290,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 +315,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 +406,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/circle.yml b/circle.yml
index 160bc7bb9..99b7d852b 100644
--- a/circle.yml
+++ b/circle.yml
@@ -14,13 +14,15 @@ database:
post:
# Initialize the settings in the database, this will create indicies for the
# database.
- - ./bin/cli settings init
+ - ./bin/cli setup --defaults
- sleep 2
test:
override:
# Run the tests using the junit reporter.
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml NPM_PACKAGE_CONFIG_MOCHA_REPORTER=mocha-junit-reporter npm run test
+ # Run the e2e test suite
+ - E2E_REPORT_PATH=$CIRCLE_TEST_REPORTS/e2e npm run e2e
deployment:
release:
diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js
index c3f9b1be1..8bc0d4e4c 100644
--- a/client/coral-admin/src/actions/auth.js
+++ b/client/coral-admin/src/actions/auth.js
@@ -14,7 +14,10 @@ export const checkLogin = () => dispatch => {
const isAdmin = !!result.user.roles.filter(i => i === 'ADMIN').length;
dispatch(checkLoginSuccess(result.user, isAdmin));
})
- .catch(error => dispatch(checkLoginFailure(error)));
+ .catch(error => {
+ console.error(error);
+ dispatch(checkLoginFailure(`${error.message}`));
+ });
};
// LogOut Actions
diff --git a/client/coral-admin/src/actions/comments.js b/client/coral-admin/src/actions/comments.js
index fe4895ed3..df68b72c4 100644
--- a/client/coral-admin/src/actions/comments.js
+++ b/client/coral-admin/src/actions/comments.js
@@ -15,17 +15,18 @@ export const fetchModerationQueueComments = () => {
return Promise.all([
coralApi('/queue/comments/pending'),
+ coralApi('/queue/users/pending'),
coralApi('/queue/comments/rejected'),
coralApi('/queue/comments/flagged')
])
- .then(([pending, rejected, flagged]) => {
+ .then(([pendingComments, pendingUsers, rejected, flagged]) => {
/* Combine seperate calls into a single object */
flagged.comments.forEach(comment => comment.flagged = true);
return {
- comments: [...pending.comments, ...rejected.comments, ...flagged.comments],
- users: [...pending.users, ...rejected.users, ...flagged.users],
- actions: [...pending.actions, ...rejected.actions, ...flagged.actions]
+ comments: [...pendingComments.comments, ...rejected.comments, ...flagged.comments],
+ users: [...pendingComments.users, ...pendingUsers.users, ...rejected.users, ...flagged.users],
+ actions: [...pendingComments.actions, ...pendingUsers.actions, ...rejected.actions, ...flagged.actions]
};
})
.then(addUsersCommentsActions.bind(this, dispatch));
@@ -41,6 +42,15 @@ export const fetchPendingQueue = () => {
};
};
+export const fetchPendingUsersQueue = () => {
+ return dispatch => {
+ dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
+
+ return coralApi('/queue/users/pending')
+ .then(addUsersCommentsActions.bind(this, dispatch));
+ };
+};
+
export const fetchRejectedQueue = () => {
return dispatch => {
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
diff --git a/client/coral-admin/src/actions/users.js b/client/coral-admin/src/actions/users.js
index 30d9cd34b..44f0325d9 100644
--- a/client/coral-admin/src/actions/users.js
+++ b/client/coral-admin/src/actions/users.js
@@ -1,5 +1,5 @@
import coralApi from '../../../coral-framework/helpers/response';
-import * as actions from '../constants/user';
+import * as userTypes from '../constants/users';
/**
* Action disptacher related to users
@@ -7,9 +7,17 @@ import * as actions from '../constants/user';
// change status of a user
export const userStatusUpdate = (status, userId, commentId) => {
return (dispatch) => {
- dispatch({type: actions.UPDATE_STATUS_REQUEST});
+ dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
- .then(res => dispatch({type: actions.UPDATE_STATUS_SUCCESS, res}))
- .catch(error => dispatch({type: actions.UPDATE_STATUS_FAILURE, error}));
+ .then(res => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
+ .catch(error => dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error}));
+ };
+};
+
+// change status of a user
+export const sendNotificationEmail = (userId, subject, body) => {
+ return (dispatch) => {
+ return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
+ .catch(error => dispatch({type: userTypes.USER_EMAIL_FAILURE, error}));
};
};
diff --git a/client/coral-admin/src/components/ActionButton.js b/client/coral-admin/src/components/ActionButton.js
new file mode 100644
index 000000000..61de1c669
--- /dev/null
+++ b/client/coral-admin/src/components/ActionButton.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import styles from './ModerationList.css';
+import I18n from 'coral-framework/modules/i18n/i18n';
+import translations from '../translations.json';
+import {FabButton, Button, Icon} from 'coral-ui';
+
+const ActionButton = ({option, type, comment = {}, user, menuOptionsMap, onClickAction, onClickShowBanDialog}) =>
+{
+ const banned = user.status === 'BANNED';
+
+ if (option === 'flag' && (type === 'USERS' || comment.status || comment.flagged === true)) {
+ return null;
+ }
+ if (option === 'ban') {
+ return (
+
+
+
+ );
+ }
+ const menuOption = menuOptionsMap[option];
+ const action = {
+ item_type: type,
+ item_id: type === 'COMMENTS' ? comment.id : user.id
+ };
+ return (
+ onClickAction(menuOption.status, type === 'COMMENTS' ? comment : user, action)}
+ />
+ );
+};
+
+export default ActionButton;
+
+const lang = new I18n(translations);
diff --git a/client/coral-admin/src/components/BanUserDialog.js b/client/coral-admin/src/components/BanUserDialog.js
index c393df21d..0ad149fbe 100644
--- a/client/coral-admin/src/components/BanUserDialog.js
+++ b/client/coral-admin/src/components/BanUserDialog.js
@@ -35,7 +35,7 @@ const BanUserDialog = ({open, handleClose, onClickBanUser, user = {}}) => (
-