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..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 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..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 c511715a5..99b7d852b 100644 --- a/circle.yml +++ b/circle.yml @@ -14,7 +14,7 @@ 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: diff --git a/client/coral-admin/src/containers/Configure/CommentSettings.js b/client/coral-admin/src/containers/Configure/CommentSettings.js index 0ccb5f820..1051bb0ba 100644 --- a/client/coral-admin/src/containers/Configure/CommentSettings.js +++ b/client/coral-admin/src/containers/Configure/CommentSettings.js @@ -32,6 +32,10 @@ const updateModeration = (updateSettings, mod) => () => { updateSettings({moderation}); }; +const updateEmailConfirmation = (updateSettings, verify) => () => { + updateSettings({requireEmailConfirmation: !verify}); +}; + const updateInfoBoxEnable = (updateSettings, infoBox) => () => { const infoBoxEnable = !infoBox; updateSettings({infoBoxEnable}); @@ -67,106 +71,123 @@ const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError return Loading settings...; } + // just putting this here for shorthand below + const on = styles.enabledSetting; + const off = styles.disabledSetting; + return (

{title}

- -
- -
-
+ +
+ +
+
{lang.t('configure.enable-pre-moderation')}

{lang.t('configure.enable-pre-moderation-text')}

-
- -
- -
-
-
{lang.t('configure.comment-count-header')}
-

- {lang.t('configure.comment-count-text-pre')} - - {lang.t('configure.comment-count-text-post')} - { - errors.charCount && - -
- - {lang.t('configure.comment-count-error')} -
- } -

-
-
- -
- -
-
- {lang.t('configure.include-comment-stream')} -

- {lang.t('configure.include-comment-stream-desc')} -

-
-
- -
+ + +
+ +
+
+
{lang.t('configure.require-email-verification')}
+

+ {lang.t('configure.require-email-verification-text')} +

+
+
+ +
+ +
+
+
{lang.t('configure.comment-count-header')}
+

+ {lang.t('configure.comment-count-text-pre')} + + {lang.t('configure.comment-count-text-post')} + { + errors.charCount && + +
+ + {lang.t('configure.comment-count-error')} +
+ } +

+
+
+ +
+ +
+
+ {lang.t('configure.include-comment-stream')} +

+ {lang.t('configure.include-comment-stream-desc')} +

+
+
+
- - -
- {lang.t('configure.closed-comments-desc')} -
- -
+
+
+ +
+ {lang.t('configure.closed-comments-desc')} +
+
- - -
- {lang.t('configure.close-after')} -
- -
- - - - - -
+
+
+ +
+ {lang.t('configure.close-after')} +
+ +
+ + + + +
- +
+
); }; diff --git a/client/coral-admin/src/containers/Streams/Streams.js b/client/coral-admin/src/containers/Streams/Streams.js index 3c4b143b8..1937f82c0 100644 --- a/client/coral-admin/src/containers/Streams/Streams.js +++ b/client/coral-admin/src/containers/Streams/Streams.js @@ -38,12 +38,13 @@ class Streams extends Component { } onSearchChange = (e) => { + const search = e.target.value; this.setState((prevState) => { - prevState.search = e.target.value; + prevState.search = search; clearTimeout(prevState.timer); const fetchAssets = this.props.fetchAssets; prevState.timer = setTimeout(() => { - fetchAssets(0, limit, e.target.value, this.state.sort, this.state.filter); + fetchAssets(0, limit, search, this.state.sort, this.state.filter); }, 350); return prevState; }); diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index a89b53523..65411066a 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -50,6 +50,8 @@ "configure": { "enable-pre-moderation": "Enable pre-moderation", "enable-pre-moderation-text": "Moderators must approve any comment before it is published.", + "require-email-verification": "Require Email Confirmation", + "require-email-verification-text": "New Users must verify their email before commenting", "include-comment-stream": "Include Comment Stream Description for Readers.", "include-comment-stream-desc": "Write a message to be added to the top of your comment stream. Pose a topic, include community guidelines, etc.", "include-text": "Include your text here.", @@ -155,6 +157,8 @@ "configure": { "enable-pre-moderation": "Habilitar pre-moderación", "enable-pre-moderation-text": "Los moderadores deben aprobar cada comentario antes de que sea publicado.", + "require-email-verification": "Necesita confirmación de correo", + "require-email-verification-text": "Nuevos usuarios deben verificar sus correos antes de comentar", "include-comment-stream": "Incluir la Descripción a un Hilo de Comentario para los y las Lectoras.", "include-comment-stream-desc": "Escribir un mensaje que será agregado a la parte de arriba del tu hilo de comentarios. Por ejemplo, un tema, guias de comunidad, etc.", "include-text": "Incluir tu texto aqui.", diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index cd937dd0d..550fc5fc1 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -5,7 +5,7 @@ import isEqual from 'lodash/isEqual'; import {TabBar, Tab, TabContent, Spinner} from 'coral-ui'; -const {logout, showSignInDialog} = authActions; +const {logout, showSignInDialog, requestConfirmEmail} = authActions; const {addNotification, clearNotification} = notificationActions; const {fetchAssetSuccess} = assetActions; @@ -181,6 +181,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ + requestConfirmEmail: () => dispatch(requestConfirmEmail()), loadAsset: (asset) => dispatch(fetchAssetSuccess(asset)), addNotification: (type, text) => { pym.sendMessage('getPosition'); diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 15ee4968b..592fe2be1 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -21,6 +21,7 @@ export const cleanState = () => ({type: actions.CLEAN_STATE}); const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST}); const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin}); const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error}); +const emailConfirmError = () => ({type: actions.EMAIL_CONFIRM_ERROR}); export const fetchSignIn = (formData) => (dispatch) => { dispatch(signInRequest()); @@ -30,7 +31,18 @@ export const fetchSignIn = (formData) => (dispatch) => { dispatch(signInSuccess(user, isAdmin)); dispatch(hideSignInDialog()); }) - .catch(() => dispatch(signInFailure(lang.t('error.emailPasswordError')))); + .catch(error => { + if (error.metadata) { + + // the user might not have a valid email. prompt the user user re-request the confirmation email + dispatch(signInFailure(lang.t('error.emailNotVerified', error.metadata))); + dispatch(emailConfirmError()); + } else { + + // invalid credentials + dispatch(signInFailure(lang.t('error.emailPasswordError'))); + } + }); }; // Sign In - Facebook @@ -137,3 +149,21 @@ export const checkLogin = () => dispatch => { dispatch(checkLoginFailure(`${error.message}`)); }); }; + +const confirmEmailRequest = () => ({type: actions.CONFIRM_EMAIL_REQUEST}); +const confirmEmailSuccess = () => ({type: actions.CONFIRM_EMAIL_SUCCESS}); +const confirmEmailFailure = () => ({type: actions.CONFIRM_EMAIL_FAILURE}); + +export const requestConfirmEmail = email => dispatch => { + dispatch(confirmEmailRequest()); + return coralApi('/users/resend-confirm', {method: 'POST', body: {email}}) + .then(() => { + dispatch(confirmEmailSuccess()); + }) + .catch(err => { + console.log('failed to send email confirmation', err); + + // email might have already been confirmed + dispatch(confirmEmailFailure()); + }); +}; diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index 5742adf75..1dae348df 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -32,3 +32,8 @@ export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS'; export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE'; export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN'; + +export const EMAIL_CONFIRM_ERROR = 'EMAIL_CONFIRM_ERROR'; +export const CONFIRM_EMAIL_REQUEST = 'CONFIRM_EMAIL_REQUEST'; +export const CONFIRM_EMAIL_SUCCESS = 'CONFIRM_EMAIL_SUCCESS'; +export const CONFIRM_EMAIL_FAILURE = 'CONFIRM_EMAIL_FAILURE'; diff --git a/client/coral-framework/helpers/response.js b/client/coral-framework/helpers/response.js index e4e6e7c37..d95773c04 100644 --- a/client/coral-framework/helpers/response.js +++ b/client/coral-framework/helpers/response.js @@ -34,15 +34,21 @@ const buildOptions = (inputOptions = {}) => { }; const handleResp = res => { - if (res.status === 401) { - throw new Error('Not Authorized to make this request'); - } else if (res.status > 399) { + if (res.status > 399) { return res.json().then(err => { let message = err.message || res.status; + const error = new Error(); + + if (err.error && err.error.metadata && err.error.metadata.message) { + error.metadata = err.error.metadata.message; + } + if (err.error && err.error.translation_key) { message = err.error.translation_key; } - throw new Error(message); + + error.message = message; + throw error; }); } else if (res.status === 204) { return res.text(); diff --git a/client/coral-framework/modules/i18n/i18n.js b/client/coral-framework/modules/i18n/i18n.js index d4ed36b09..08fc1c451 100644 --- a/client/coral-framework/modules/i18n/i18n.js +++ b/client/coral-framework/modules/i18n/i18n.js @@ -70,7 +70,7 @@ class i18n { }; this.timeago = (time) => { - return this.timeagoInstance.format(time); + return this.timeagoInstance.format(new Date(time)); }; } } diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 36f8e0764..6cb4e97cd 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -11,6 +11,9 @@ const initialState = Map({ error: '', passwordRequestSuccess: null, passwordRequestFailure: null, + emailConfirmationFailure: false, + emailConfirmationLoading: false, + emailConfirmationSuccess: false, successSignUp: false }); @@ -33,6 +36,9 @@ export default function auth (state = initialState, action) { error: '', passwordRequestFailure: null, passwordRequestSuccess: null, + emailConfirmationFailure: false, + emailConfirmationSuccess: false, + emailConfirmationLoading: false, successSignUp: false })); case actions.CHANGE_VIEW : @@ -101,6 +107,16 @@ export default function auth (state = initialState, action) { return state .set('passwordRequestFailure', 'There was an error sending your password reset email. Please try again soon!') .set('passwordRequestSuccess', null); + case actions.EMAIL_CONFIRM_ERROR: + return state + .set('emailConfirmationFailure', true) + .set('emailConfirmationLoading', false); + case actions.CONFIRM_EMAIL_REQUEST: + return state.set('emailConfirmationLoading', true); + case actions.CONFIRM_EMAIL_SUCCESS: + return state + .set('emailConfirmationSuccess', true) + .set('emailConfirmationLoading', false); default : return state; } diff --git a/client/coral-framework/translations.json b/client/coral-framework/translations.json index 5bf9b40a9..07b719f6b 100644 --- a/client/coral-framework/translations.json +++ b/client/coral-framework/translations.json @@ -5,6 +5,7 @@ "contentNotAvailable": "This content is not available", "suspendedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Flag, or write comments. Please contact moderator@fakeurl.com for more information", "error": { + "emailNotVerified": "Email address {0} not verified.", "email": "Not a valid E-Mail", "password": "Password must be at least 8 characters", "displayName": "Display names can contain letters, numbers and _ only", @@ -27,6 +28,7 @@ "contentNotAvailable": "El contenido no se encuentra disponible", "suspendedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios. Por favor, contacta moderator@fakeurl for more information", "error": { + "emailNotVerified": "Dirección de correo electrónico {0} no verificada.", "email": "No es un email válido", "password": "La contraseña debe tener por lo menos 8 caracteres", "displayName": "Los nombres pueden contener letras, números y _", diff --git a/client/coral-sign-in/components/SignDialog.js b/client/coral-sign-in/components/SignDialog.js index 6243472b4..0645f110f 100644 --- a/client/coral-sign-in/components/SignDialog.js +++ b/client/coral-sign-in/components/SignDialog.js @@ -17,7 +17,12 @@ const SignDialog = ({open, view, handleClose, offset, ...props}) => ( }}> × {view === 'SIGNIN' && } - {view === 'SIGNUP' && } + { + view === 'SIGNUP' && + } {view === 'FORGOT' && } ); diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js index de5e77a49..e27185b52 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -1,67 +1,100 @@ -import React from 'react'; -import Button from 'coral-ui/components/Button'; -import FormField from './FormField'; +import React, {PropTypes} from 'react'; import Alert from './Alert'; -import Spinner from 'coral-ui/components/Spinner'; +import {Button, FormField, Spinner, Success} from 'coral-ui'; import styles from './styles.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; const lang = new I18n(translations); -const SignInContent = ({handleChange, formData, ...props}) => ( -
-
-

- {lang.t('signIn.signIn')} -

-
-
- -
-
-

- {lang.t('signIn.or')} -

-
- { props.auth.error && {props.auth.error} } -
- - -
- { - !props.auth.isLoading ? - - : - - } +const SignInContent = ({ + handleChange, + handleChangeEmail, + emailToBeResent, + handleResendConfirmation, + emailConfirmationLoading, + emailConfirmationSuccess, + formData, + ...props +}) => { + + return ( +
+
+

+ {props.auth.emailConfirmationFailure ? lang.t('signIn.emailConfirmCTA') : lang.t('signIn.signIn')} +

+
+
+ +
+
+

+ {lang.t('signIn.or')} +

+
+ { props.auth.error && {props.auth.error} } + { + props.auth.emailConfirmationFailure + ? +

{lang.t('signIn.requestNewConfirmEmail')}

+ + + {emailConfirmationLoading && } + {emailConfirmationSuccess && } + + :
+ + +
+ { + !props.auth.isLoading ? + + : + + } +
+ + } + - - -
-); + ); +}; + +SignInContent.propTypes = { + emailConfirmationLoading: PropTypes.bool.isRequired, + emailConfirmationSuccess: PropTypes.bool.isRequired, + handleResendConfirmation: PropTypes.func.isRequired, + handleChangeEmail: PropTypes.func.isRequired, + emailToBeResent: PropTypes.string.isRequired +}; export default SignInContent; diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js index 4144b1d2f..246d9d884 100644 --- a/client/coral-sign-in/components/SignUpContent.js +++ b/client/coral-sign-in/components/SignUpContent.js @@ -1,9 +1,6 @@ import React from 'react'; -import FormField from './FormField'; import Alert from './Alert'; -import Button from 'coral-ui/components/Button'; -import Spinner from 'coral-ui/components/Spinner'; -import Success from 'coral-ui/components/Success'; +import {Button, FormField, Spinner, Success} from 'coral-ui'; import styles from './styles.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; diff --git a/client/coral-sign-in/components/styles.css b/client/coral-sign-in/components/styles.css index d4ca8c309..60a252a20 100644 --- a/client/coral-sign-in/components/styles.css +++ b/client/coral-sign-in/components/styles.css @@ -14,28 +14,6 @@ font-size: 1.2em; } -.formField { - margin-top: 15px; -} - -.formField label { - font-size: 1.08em; - font-weight: bold; - margin-bottom: 5px; -} - -.formField input { - width: 100%; - display: block; - border: none; - outline: none; - border: 1px solid rgba(0,0,0,.12); - padding: 10px 6px; - box-sizing: border-box; - border-radius: 2px; - margin: 5px auto; -} - .footer { margin: 20px auto 10px; text-align: center; @@ -153,3 +131,15 @@ input.error{ background-color: 1px solid coral; padding: 10px; } + +.emailConfirmDialog { + margin-top: 15px; +} + +.confirmLabel { + display: block; +} + +.confirmSubmit { + +} diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 9e9ca1f72..2435cdcfd 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -16,6 +16,7 @@ import { hideSignInDialog, fetchSignInFacebook, fetchForgotPassword, + requestConfirmEmail, facebookCallback, invalidForm, validForm, @@ -30,6 +31,7 @@ class SignInContainer extends Component { password: '', confirmPassword: '' }, + emailToBeResent: '', errors: {}, showErrors: false }; @@ -38,9 +40,10 @@ class SignInContainer extends Component { super(props); this.state = this.initialState; this.handleChange = this.handleChange.bind(this); + this.handleChangeEmail = this.handleChangeEmail.bind(this); + this.handleResendConfirmation = this.handleResendConfirmation.bind(this); this.handleSignUp = this.handleSignUp.bind(this); this.handleSignIn = this.handleSignIn.bind(this); - this.handleClose = this.handleClose.bind(this); this.addError = this.addError.bind(this); } @@ -71,6 +74,23 @@ class SignInContainer extends Component { }); } + handleChangeEmail(e) { + const {value} = e.target; + this.setState({emailToBeResent: value}); + } + + handleResendConfirmation(e) { + e.preventDefault(); + this.props.requestConfirmEmail(this.state.emailToBeResent) + .then(() => { + setTimeout(() => { + + // allow success UI to be shown for a second, and then close the modal + this.props.handleClose(); + }, 2500); + }); + } + addError(name, error) { return this.setState(state => ({ errors: { @@ -124,12 +144,9 @@ class SignInContainer extends Component { this.props.fetchSignIn(this.state.formData); } - handleClose() { - this.props.hideSignInDialog(); - } - render() { const {auth, showSignInDialog, noButton, offset} = this.props; + const {emailConfirmationLoading, emailConfirmationSuccess} = auth; return (
{!noButton &&